vic

High performance video graphic library for embedded systems

View on GitHub

Video Interface Controller library

Release CI C++11 Header only No heap Compilers Last commit Issues Stars License: MIT

A universal, lightweight, extremly fast and powerful C++ library for driving graphic, dot, alpha numeric and seven-segment displays in embedded systems

Highlights and Design Goals

What is VIC not?

VIC is not meant to be a fully loaded window manager, widgets, dialogs and theme rendering framework. If you need an advanced windowed GUI, there are many other cool libraries around, like LVGL, µC/GUI, StWin etc. But such a (high level) library may use VIC as its rendering and display driver layer via the blit() command.

VIC or a GUI framework like LVGL? VIC is a graphic/widget rendering and display driver layer, not a GUI. If your display is a TFT, your controller has RAM for a framebuffer and you need advanced controls, themes, a GUI framework is the right tool. VIC is made for the other cases: Write only or serial displays, controllers with their own GRAM and native fill commands, monochrome and odd devices (OLEDs, LED matrices, VFD text displays, seven segment), heterogeneous multihead setups and MCUs with a few KB of RAM. Both can be combined: a GUI framework’s flush callback is exactly a blit(), so a VIC head can serve as its display driver.

Design of VIC

Most display libraries render into a RAM framebuffer and transfer the complete buffer to the display afterwards. This works fine for memory mapped displays, but embedded displays are often connected via SPI, I²C or UART with a few MBit/s or even kBit/s. Transferring a complete framebuffer over such a link takes far too long for fast updates, and small microcontrollers don’t have the RAM for a big framebuffer anyway.

VIC therefore renders directly to the display and tries to send as few commands as possible:

Displays which can’t read their pixels back (most serial displays) return color::none from pixel_get(). The drawing context then blends alpha and anti aliasing pixels against its background color once one is set with dc::set_bg_color(), which is exact on uniform backgrounds and needs no RAM. If the real pixels are needed (sprites, fill, anti aliasing on images), put the framebuffer head between the display driver and the drawing context. It keeps a RAM copy in the format you choose (ARGB8888, RGB565, RGB332 or 1 bit L1), remembers the dirty rect and transfers only the changed window with blit() on present(). This is also the “render in RAM, stream the changed window” model modern TFT controllers with an autoincrementing GRAM window are made for, and it is the user’s decision per display, the device drivers themselves keep no buffers.

A 240 x 320 ST7789 over SPI can’t read its GRAM back, so sprites and anti aliasing on a gradient won’t blend. A framebuffer head in front of the display driver fixes that, the drawing code stays the same:

vic::head::MIPI_DCS<vic::head::MIPI_DCS_ST7789, 240, 320> lcd(spi_handle);     // the write only display
vic::head::framebuffer<240, 320, 1, vic::color::format_RGB565> fb(lcd);         // 150 KB RAM copy, blits the dirty rect to lcd
vic::dc _dc(fb);                                                                // draw into the framebuffer, not into lcd
fb.init();                                                                      // initializes the lcd as well

_dc.anti_aliasing_enable();
_dc.alpha_blending_enable();
_dc.present_lock();                                   // collect a whole frame, don't transfer after every primitive
_dc.set_color(vic::color::blue);
_dc.disc({ 120, 160 }, 50);                           // the edge pixels are blended against the RAM copy
_dc.set_color(vic::color::argb(0xFF, 0xFF, 0xFF, 0x80));   // white, alpha 128
_dc.box({ 20, 20, 220, 60 });                         // half transparent box over the disc, needs the pixels below
_dc.present_lock(false);                              // transfers the dirty rect (one GRAM window) to the display

Without the framebuffer head and a uniform background _dc.set_bg_color(vic::color::black) gives the same anti aliasing result for the disc without any RAM, only the transparent box can’t be blended then.

Class design

The hardware graphic display driver is called head here and only needs to implement pixel_set and pixel_get functions. But most modern display controllers can provide more sophisticated functions for native line/box rendering, block moving (blit) etc. If a controller can provide such special functions, the according function is implemented in the driver by simply overriding the virtual function of the drv base class. All graphic functions which the controller/driver can’t provide natively are handled by the gpr. Of cource, native rendering on a specialized LCD controller is always faster and should be prefered.

The native hooks are deliberately limited to primitives which have exactly one correct pixel result: line_horz, line_vert, box, move, cls and blit (a block of pixels). Everything else (diagonal lines, circles, discs, sectors, triangles, text) is decomposed by the gpr into these hooks, so every head renders exactly the same pixels and the anti aliasing stays deterministic: the interior of an anti aliased disc is still sent as native horizontal runs, only the edge pixels are single alpha pixels. There are no hooks for native diagonal lines or circles on purpose, a controller’s own algorithm would make the result head dependent.

gpr

The Graphic Primitive Renderer provides fast rendering functions for all basic primitives like lines, circles, triangles, discs, pies, pattern fill, etc. The gpr doesn’t use any floating point math, only fast integer operations. Furthermore the gpr should be a collection of the fastest implementations of the most modern, fast and reliable primitive rendering algorithms. So, if you find any errors, have speed improvements or even a better algorithm for some primitives - please share and contribure!

All rects (rect_type) are right/bottom exclusive, like { left, top, right, bottom } covers the pixels left <= x < right and top <= y < bottom. Angles are given in degrees, 0° is at 12 o’clock and angles run clockwise. Angle intervals like in sector() are half open ([start, end)), so adjacent sectors tile without gaps or double drawn pixels.

txr

The Text Renderer is responsible for rendering text fonts on graphic displays. It supports antialiasing, monospaced and proportional fonts.

Fonts are header files in src/font/, a font is a class with one info() method which returns the glyph data (font::font_type, see src/font.h). Available fonts:

New fonts are generated from any TTF/OTF file with tools/fontgen.py (Python 3 with freetype-py, Pillow for the optional preview), the number is the line height in pixel and --bpp the gray levels (1 = mono, 2 or 4 = anti aliased):

python tools/fontgen.py src/font/ttf/Montserrat-Regular.ttf --size 12 --bpp 2
python tools/fontgen.py myfont.ttf --size 20 --bpp 4 --ranges 20-7E --name MyFont_20 --preview myfont.png

The glyphs are hinted with the FreeType auto hinter and stems thinner than a pixel are darkened, so a stem is one solid pixel column instead of two gray ones (--darken, automatic by default). tools/gen_fonts.cmd regenerates all fonts above. Rule of thumb for the flash size: about glyphs * (height * width * bpp / 8 + 8) byte, e.g. 190 glyphs of a 12 px 2 bit font take about 5 KB, a 24 px 4 bit font about 30 KB. Restrict the ranges (--ranges 30-39 for digits) if a display only shows numbers.

dc

To draw anything on the screen, at least one Drawing Context is necessary. The dc stores information about the pen color, the anti aliasing setting and has an associated pipe of shaders. Shaders are extremly fast and modify the rendered pixel (or its position) before it is sent to the head driver. Several drawing contexts can share one head.

Available stock shaders are:

tc

The Text Context is the counterpart of the dc for text only (Alpha Numeric) displays. It renders UTF-8 strings with the selected font, including control chars like \n, \r and the inline color escape \aRRGGBB.

drv

The base class of the custom head driver which adds some mandatory driver routines. All vic color calculations are done in ARGB 32 bit format. If a display has less colors, like 15 bit RGB, the specific driver class is responsible for color reduction. Conversion routines provides the color class.

The specific display driver. A display together with its controller is referenced here as head. And head is the namespace of all specific drivers. A head driver is platform independent can run on any system.
Available heads are:
| Head | Display | |—|—| | BA6x | Wincor Nixdorf BA60 / BA63 / BA66 customer displays | | HD44780 | HD44780 / ST7066 / KS0066 Alphanumeric character LCDs, 4 or 8 bit bus | | MAX7219 | MAX7219 / MAX7221 8 x 8 LED matrices | | Digole | Digole serial graphic modules | | ILI9325 | ILI9325 TFT controller | | MIPI_DCS | MIPI DCS TFT controllers (ILI9341, ILI9488, ST7735, ST7789, ST7796, GC9A01, JD9853 …) | | PCD8544 | Nokia 5110 / 3310 LCD | | SSD1306 | SSD1306 / SSD1309 / SH1106 monochrome OLEDs | | SSD1322 | SSD1322 4 bit grayscale OLEDs | | SSD1331, SSD1351 | 96 x 64 and 128 x 128 RGB565 OLEDs | | SSD1680 | SSD1680 / SSD1681 e-paper | | ST7565 | ST7565 / ST7567 / UC1701 monochrome COG LCDs | | ST7920 | ST7920 monochrome graphic LCD | | STPxxP05 | STP08xP05 / STP16xP05 / STP24DP05 LED array drivers | | seven_segment | Seven segment displays | | framebuffer | RAM framebuffer with dirty rect transfer in front of any other head, dirty rect transfer | | multihead | Combines several heads to one screen | | memory | Pure RAM head with statistics, ideal for unit tests and off screen rendering | | dummy | Null head | | windows | Emulates a display in a Win32 window | | skeleton | Template for new heads |

skeleton.h is the template for new heads. src/head/doc/ holds the controller datasheets and a programming summary (*.md) of each one with the register / command tables, the orientation bits and what the head sends.

sprite

I have always been fascinated by hardware sprites that computers like the Commodore 64 or Amiga offered. When designing VIC, I wanted to create a simple but fast and flexible sprite rendering engine with collision detection, rotation and unlimited sprite layers.
Sprites are moving objects which restore their background when they move. sprite::sheet renders frames out of a constant sprite sheet (RGB565, RGB888, ARGB8888, RGBA8888) and needs no RAM for the pattern, sprite::canvas is a sprite which can be drawn on with the normal dc functions and sprite::canvas_sheet loads sheet frames into a canvas. Canvas sprites can be rotated by any angle at zero cost when no rotation is used. Overlapping sprites are rendered in z-order with alpha blending. Pixel exact collision detection comes for free: set a collision handler (set_collision_handler()) on a sprite and it is called once for every other sprite it overlaps with when it is rendered, or poll get_collision() after render(). Without a handler the detection costs nothing.

The demo/demo_sprite.cpp is completely rendered by VIC primitives and shows sheet sprites, a canvas sprite (the ball), rotation, alpha blending, z-order and the collision detection (the ghosts turn red when they meet):

widget

Simple widgets which are useful in nearly every embedded project: widget::bitmap renders raw bitmaps (L1, RGB332, RGB555, RGB565, RGB888, ARGB8888) and XPM3 images, widget::progress is a progress bar with four orientations and delta drawing. For instrument style displays there are widget::gauge (round arc gauge with colored bands, ticks and a needle), widget::bargauge (linear band strip with up to two triangle markers and an optional fill bar), widget::label (text or fixed point numeric readout in a cell, redrawn only when the text changes), widget::textbox (multi line text with word wrap and line scrolling), widget::groupbox (titled frame), widget::led (round or square indicator), widget::arc (ring progress, delta drawn) and widget::spinner (rotating ring segment as busy indicator). The needle and the markers are canvas sprites, so a value change costs only their pixels and the background restores itself. widget::compass is a heading up compass rose with a fixed pointer.

Input routing is one function: the application calls widget::base::event({x, y}, widget::event_tap) from its touch, mouse or button driver with the position and the event (event_down, event_up, event_tap, event_long_tap). The widget under the position reacts first (the one created last wins when widgets overlap) and then calls the handler set with set_event_handler(). The input widgets build on it: widget::button and widget::imagebutton (icon and caption, soft keys with an active state) show a pressed look between down and up, widget::checkbox and widget::toggle (a switch) toggle on a tap, widget::slider takes its value from the tap position and widget::spinbox steps its value with its minus and plus cells (a long tap is ten steps). demo/demo_input.cpp is a settings page operated by a simulated touch. There is no focus, no styles and no theme engine, full featured GUIs belong to a library on top of VIC.

demo/demo_widget.cpp shows every widget with its specific features (orientations, border, bitmap formats, alignment, color escapes, bands, ticks, fill mode, dual markers, anti aliasing, the input widgets operated by a simulated touch) on three pages, 30 s recording:

demo/demo_gauges.cpp shows the gauge widgets on an aircraft engine page:

demo/demo_input.cpp is a settings page with checkboxes, a switch, a slider, a spinbox, buttons, a compass and a soft key bar, operated by a simulated touch through widget::base::event():

Anti aliasing

Anti aliasing is switched on per drawing context with dc.anti_aliasing_enable(true) (and off again with false). It is off by default and the normal rendering is not affected by it at all.

The principle is pixel coverage: every pixel which is only partly covered by the primitive gets the pen color with an alpha channel of pen alpha * coverage / 255. This alpha pixel takes the normal alpha path of vic, so the output shader blends it with the pixel the head reads back, and a canvas sprite blends it into its pattern. No floating point math is used anywhere.

Costs: the edge pixels cost CPU time (16 shape tests per edge pixel, 9 per interior pixel inside the bounding box) and every edge pixel needs the background pixel for the alpha blending. On a write only serial display (no pixel_get support) the output shader can’t blend, so use anti aliasing there with the framebuffer head, with a head that keeps its own framebuffer (like the SSD1306 head) or on canvas sprites which keep their own pixels anyway. Coordinates have to stay within +/- 4000 pixels for the 32 bit sub pixel math.

vic::dc _dc(_head);
_dc.anti_aliasing_enable();                   // from now on edges are smooth
_dc.set_color(vic::color::white);
_dc.line({ 10, 10 }, { 100, 40 });            // Wu line
_dc.disc({ 60, 60 }, 25U);                    // disc with coverage sampled edge
_dc.sector({ 60, 60 }, 10U, 25U, 30U, 150U);  // ring piece, radial and angular edges are smooth
_dc.anti_aliasing_enable(false);              // back to plain pixels

demo/demo_aa.cpp renders the same scene twice, left without and right with anti aliasing, with a 3x magnifier of the moving clock hand (30 s recording of the demo):

Usage

Using VIC is really easy and fun. Add the src folder to your include path (or link the vic::vic CMake interface target) and provide the I/O functions declared in io.h for your platform (SPI/I²C/UART/memory access, delay). In a single head design (just one display in the system) you create your head first by instancing the according driver. E.g. on a Windows (emulation/test) platform this would be:

#include "head/Windows.h"

// Create a screen of 240 * 120 pixel with a viewport of the same size (240 * 120)
// The viewport has an offset of 0,0 and the window is placed at 100,100 on the windows desktop
// For development convinience a horizontal and vertical zoom factor of 4 is selected
vic::head::windows<240, 120, 240, 120> _head(0U, 0U, 100U, 100U, 4U, 4U);
_head.init();  // explicit head init

Drawing is done with a drawing context (dc) and text is written with a text context (tc) on the head:

#include "dc.h"
#include "tc.h"
#include "font/Arial_num_16x24_AA4.h"

vic::dc _dc(_head);
_dc.set_color(vic::color::brightblue);    // select bright blue drawing color
_dc.line({ 10, 10 }, { 40, 40 });         // draw a line from 10,10 to 40,40
_dc.box({ 50, 10, 90, 30 }, 4U);          // filled box with rounded corners, right/bottom exclusive
_dc.disc({ 120, 60 }, 20U);               // filled circle

// write some text
vic::tc _tc(_head);
vic::font::Arial_num_16x24_AA4 _font;
_tc.set_font(_font);
_tc.set_color(vic::color::yellow);
_tc.out({ 10, 50 }, (const std::uint8_t*)"12,34.56-7890");

If you need multiple displays in your design, we are talking about a “multiple heads” scenario. All heads are independent. Let’s create two independent heads with their own drawing contexts:

vic::head::windows<240, 120, 240, 120> _head0(0U, 0U, 0U,   0U, 2U, 2U);
vic::head::windows<240, 120, 240, 120> _head1(0U, 0U, 0U, 200U, 2U, 2U);
_head0.init();  // explicit head 0 init
_head1.init();  // explicit head 1 init

vic::dc _dc0(_head0);
vic::dc _dc1(_head1);
_dc0.set_color(vic::color::brightblue);
_dc0.line({ 10, 10 }, { 40, 40 });
_dc1.set_color(vic::color::red);           // eg. for system debug, status etc.
_dc1.box({ 100, 100, 120, 120 });

Or you can combine multiple single displays to one big screen. It’s a common way to create big screens out of single modules. This is done by using the multihead driver.

#include "head/MAX7219.h"
#include "head/multihead.h"

// Create 3 MAX7219 heads of 8 * 8 pixels on the SPI device handles spi1, spi2 and spi3
vic::head::MAX7219<8U, 8U> _head0(spi1);
vic::head::MAX7219<8U, 8U> _head1(spi2);
vic::head::MAX7219<8U, 8U> _head2(spi3);
// Create the multihead out of the three heads with offsets of 0, 8 and 16 pixels
// So the multihead has a screen resolution of 24 * 8 pixels
vic::head::multihead<24U, 8U, 3U> _multihead = { { _head0, { 0, 0 } }, { _head1, { 8, 0 } }, { _head2, { 16, 0 } } };

// Now, use the multihead like a normal head
vic::dc _dc(_multihead);
_dc.line({ 0, 0 }, { 23, 7 });

Shaders are registered at a drawing context and transform every pixel on its way to the head:

#include "shader/base.h"
#include "shader/gradient.h"

vic::shader::clipping _clip({ 0, 0, 100, 50 });          // draw only inside this rect
vic::shader::gradient<2U> _gradient = { { { 10, 25 }, vic::color::red },   // color points, the pixel color is
                                        { { 90, 25 }, vic::color::blue } }; // interpolated by distance
_dc.shader_register(&_clip);
_dc.shader_register(&_gradient);
_dc.disc({ 50, 25 }, 40U);                               // clipped and gradient filled
_dc.shader_remove(&_gradient);

Command reference

A compact overview of the public API. All coordinates are vertex_type { x, y } (int16), rects are rect_type { left, top, right, bottom } with exclusive right/bottom, colors are color::value_type (ARGB8888). Angles of circle() / sector() are degree, 0° at 3 o’clock, counter clockwise, half open [start, end).

Drawing context vic::dc (dc.h, derived from gpr)

dc(drv& head);                                            // drawing context on a head (or a sprite canvas)

// state
void set_color(color::value_type color);                  // pen color
color::value_type get_color() const;
void set_bg_color(color::value_type bg);                  // blend background for write-only heads
void anti_aliasing_enable(bool enable = true);
void alpha_blending_enable(bool enable = true);

// pixels and lines
void pixel_set(vertex_type v);                            // without present()
void pixel_set(vertex_type v, color::value_type c);
color::value_type pixel_get(vertex_type v);
void plot(vertex_type v);                                 // pixel_set() + present()
void line(vertex_type v0, vertex_type v1);                // both end points included, Wu with AA
void line_horz(vertex_type v0, vertex_type v1);
void line_vert(vertex_type v0, vertex_type v1);
void polyline(const vertex_type* vertices, std::size_t count);

// shapes
void box(const rect_type& rect);                          // filled rectangle
void box(const rect_type& rect, std::uint16_t border_radius);
void box(vertex_type v0, vertex_type v1);
void rectangle(const rect_type& rect);                    // outline
void rectangle(const rect_type& rect, std::uint16_t border_radius);
void triangle(vertex_type v0, vertex_type v1, vertex_type v2);
void triangle_solid(vertex_type v0, vertex_type v1, vertex_type v2);
void circle(vertex_type center, std::uint16_t radius, std::uint16_t start_angle = 0, std::uint16_t end_angle = 360);
void disc(vertex_type center, std::uint16_t radius);
void disc_sector(vertex_type center, std::uint16_t radius, std::uint8_t quadrant);   // 0 TR, 1 TL, 2 BL, 3 BR
void sector(vertex_type center, std::uint16_t inner_radius, std::uint16_t outer_radius,
            std::uint16_t start_angle, std::uint16_t end_angle);                     // pie or ring segment
void fill(vertex_type start, color::value_type color, bool use_color_as_bounding = true);   // flood fill

// screen
void cls(color::value_type bg_color = color::none);
void move(const rect_type& source, vertex_type destination);
void blit(const rect_type& rect, const color::value_type* pixels, std::size_t stride);
void present();                                           // transfer to the display
void present_lock(bool lock = true);                      // nested, the last unlock calls present()
std::uint16_t screen_width() const;
std::uint16_t screen_height() const;

// shader pipe
void shader_register(shader::base* shader);               // inserted at the front of the pipe
bool shader_remove(const shader::base* shader);
void shader_init();                                       // remove all shaders
bool shader_is_active() const;

Text context vic::tc (tc.h, derived from txr)

tc(dc& _dc);                                              // text on a graphic head through a dc
tc(drv& head);                                            // text on an alpha numeric head
void set_font(const font::font_type& font);
void set_color(color::value_type color);
void set_pos(vertex_type pos);                            // pixel (graphic) or character cell (alpha)
void set_inverse(bool inverse = true);
std::uint16_t out(const std::uint8_t* utf8);              // \n, \r and \aRRGGBB color escapes, returns the chars
std::uint16_t out(vertex_type pos, const std::uint8_t* utf8);
void out(std::uint16_t ch);                               // one character
std::uint16_t get_extend(rect_type& rect, const std::uint8_t* utf8) const;   // size of a line without rendering
std::uint8_t char_width() const;
std::uint8_t char_height() const;
void cls(color::value_type bg_color = color::none);
void clear_eol(); void clear_sol(); void clear_line();    // alpha numeric heads
void present(); void present_lock(bool lock = true);

Driver base vic::drv (drv.h, every head)

void init();  void shutdown();
const char* version() const;
bool is_graphic() const;
std::uint16_t screen_width() const;  std::uint16_t screen_height() const;
std::uint16_t viewport_width() const;  std::uint16_t viewport_height() const;
void viewport_set(vertex_type v);  vertex_type viewport_get() const;
bool screen_is_inside(vertex_type v) const;  bool viewport_is_inside(vertex_type v) const;
void display_enable(bool enable = true);
void display_brightness(std::uint8_t level);
void display_contrast(std::uint8_t level);
std::size_t framebuffer_get_count() const;
bool framebuffer_set_access(std::size_t plane);
bool framebuffer_set_display(std::size_t plane, std::uint8_t alpha = 0);

Sprites vic::sprite (sprite.h)

canvas<Pixel, Frames>(drv& head, std::int16_t z_index = 0);   // dynamic sprite, drawn with the dc functions
sheet<Width, Height>(drv& head, const sheet_info_type* sheet, std::int16_t z_index = 0);   // frames from a sprite sheet
canvas_sheet<Pixel>(drv& head, const sheet_info_type* sheet, std::int16_t z_index = 0);   // sheet frames, rotatable

void render(vertex_type position, std::uint16_t frame = 0);   // move / show, an invalid frame (0xFFFF) hides
vertex_type get_position() const;
std::int16_t get_z_index() const;
void set_rotation(std::int16_t angle, vertex_type center = { 0, 0 });   // canvas, degree, clockwise
void select_frame(std::uint16_t frame);                   // canvas: edit frame
void cls();                                               // canvas: clear the edit frame
std::uint16_t frame_count() const;  std::uint16_t pixel_count() const;
void set_collision_handler(collision_handler_type handler, void* arg = nullptr);
base* get_collision() const;

Shaders vic::shader (shader/*.h)

| Shader | Function | |—|—| | clipping(rect, inside = true) | clip to or out of a rect | | zoom(x_level, y_level) | integer zoom | | offset(vertex) | move everything by an offset | | flip() | mirror horizontally / vertically around an axis | | rotate(center, angle) | rotation in degree | | tint(color, weight) | tint every pixel | | alpha_blend() | blend with the background | | brush() | pen shapes (solid, dash, dot, dashdot) of 1 to 4 pixel | | gradient<N>, gradient_horizontal<N>, gradient_vertical<N>, gradient_solid<N> | color gradients from N points |

Widgets vic::widget (widget/*.h)

Every widget takes the head and a config_type struct, init() draws it. | Widget | Main functions | |—|—| | progress | set_pos(), set_range(), set_step(), step_it(), set_color() | | bitmap | render_raw(origin, w, h, format, data), render_xpm(origin, xpm) | | label | set_text(), set_value(value, decimals, suffix), set_color() | | textbox | set_text(), scroll(), set_first_line(), get_line_count() | | groupbox | set_title() | | led | set_state(), toggle(), set_color() | | gauge<> | set_value(), set_range(), anti_aliasing_enable() | | bargauge<> | set_value(marker, value), set_range() | | arc | set_value(), set_range(), set_color(), anti_aliasing_enable() | | spinner | step(), set_pos(), anti_aliasing_enable() | | compass | set_heading(), anti_aliasing_enable() | | button, imagebutton | pressed look on down / up, set_caption(), set_active() (imagebutton) | | checkbox | set_checked(), toggle(), toggles on tap | | toggle (switch) | set_state(), toggle_state(), toggles on tap | | slider | set_value(), set_range(), value follows the tap / down position | | spinbox | set_value(), set_range(), set_step(), tap / long tap on the minus and plus cells |

Input routing:

widget::base::event(vertex_type pos, widget::event_type ev);   // event_down, event_up, event_tap, event_long_tap
void set_event_handler(event_handler_type handler, void* arg = nullptr);   // per widget callback
void event_enable(bool enable = true);                    // exclude a widget from the routing
widget::base* find(vertex_type pos);                      // hit test

Colors and helpers (color.h, util.h)

color::argb(r, g, b, a = 255);  color::hsv(hue, saturation, value, alpha = 255);
color::get_red() / get_green() / get_blue() / get_alpha();  color::set_alpha(color, alpha);
color::mix(fg, bg, lum);  color::dim(color, lum);  color::alpha_blend(front, back);
color::black, white, gray, gray01 .. gray15, red, green, blue, yellow, cyan, magenta, orange, ..., none
util::sin(degree);  util::cos(degree);                    // * 16384, integer
util::vertex_rotate(point, center, degree);               // clockwise on screen
util::isqrt(value);  util::distance_squared(v0, v1);  util::abs(value);  util::min2() / max2()

Building and testing

VIC itself is header only and needs no build. The repository contains a CMake project which builds the unit tests (test/) and, on Windows, the demos (demo/): a clock, a sprite demo (demo_sprite.cpp with sprite sheets, rotation and anti aliasing), the anti aliasing comparison (demo_aa.cpp), a font demo (demo_font.cpp, cycles through all fonts with a pangram, digits and Latin-1 samples), the widget demo (demo_widget.cpp, every widget with its features), the gauge demo (demo_gauges.cpp, an aircraft engine page) and the input demo (demo_input.cpp, a settings page operated by a simulated touch).

demo/heads/ holds one small portable program per head (vic_head_<name>): it instantiates the head for a common module with the orientation the module needs, draws a test scene and runs with the tracing io layer demo/heads/io_trace.cpp, which has no hardware behind it and dumps the command / data stream to stdout. Copy a head demo and replace the four vic::io functions with your bus code to bring up a display.

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure

On Windows just run build.cmd [Debug|Release] from a normal shell, it locates Visual Studio and does the above. The tests are compiled with /W4 /WX /permissive- (MSVC) resp. -Wall -Wextra -Wpedantic -Wshadow -Werror in strict C++11 mode (GCC, Clang); -DVIC_TEST_ASAN=ON builds them with address and undefined behaviour sanitizers. The test framework (test/vic_test.h) is self contained, the tests render on the memory head and compare the result with ASCII art rasters, so new primitives and heads are easy to test.

Projects Using VIC

Contributing

  1. Give this project a :star:
  2. Create an issue and describe your idea
  3. Fork it
  4. Create your feature branch (git checkout -b my-new-feature)
  5. Commit your changes (git commit -am 'Add some feature')
  6. Publish the branch (git push origin my-new-feature)
  7. Create a new pull request
  8. Profit! :heavy_check_mark:

License

VIC is written under the MIT licence.