Tech News
← Back to articles

Canvas_ity: A tiny, single-header <canvas>-like 2D rasterizer for C++

read original related products more articles

This is a tiny, single-header C++ library for rasterizing immediate-mode 2D vector graphics, closely modeled on the basic W3C (not WHATWG) HTML5 2D canvas specification.

The priorities for this library are high-quality rendering, ease of use, and compact size. Speed is important too, but secondary to the other priorities. Notably, this library takes an opinionated approach and does not provide options for trading off quality for speed.

Despite its small size, it supports nearly everything listed in the W3C HTML5 2D canvas specification, except for hit regions and getting certain properties. The main differences lie in the surface-level API to make this easier for C++ use, while the underlying implementation is carefully based on the specification. In particular, stroke, fill, gradient, pattern, image, and font styles are specified slightly differently (avoiding strings and auxiliary classes). Nonetheless, the goal is that this library could produce a conforming HTML5 2D canvas implementation if wrapped in a thin layer of JavaScript bindings. See the accompanying C++ automated test suite and its HTML5 port for a mapping between the APIs and a comparison of this library's rendering output against browser canvas implementations.

📝 Example

The following complete example program writes out a TGA image file and demonstrates path building, fills, strokes, line dash patterns, line joins, line caps, linear gradients, drop shadows, and compositing operations. See the HTML5 equivalent of the example on the right (scroll the code horizontally if needed) and compare them line-by-line. Note that the minor differences in shading are due to the library's use of gamma-correct blending whereas browsers typically ignore this.

canvas_ity HTML5 # include < algorithm > # include < fstream > // Include the library header and implementation. # define CANVAS_ITY_IMPLEMENTATION # include " canvas_ity.hpp " int main () { // Construct the canvas. static int const width = 256 , height = 256 ; canvas_ity::canvas context ( width, height ); // Build a star path. context. move_to ( 128 . 0f , 28 . 0f ); context. line_to ( 157 . 0f , 87 . 0f ); context. line_to ( 223 . 0f , 97 . 0f ); context. line_to ( 175 . 0f , 143 . 0f ); context. line_to ( 186 . 0f , 208 . 0f ); context. line_to ( 128 . 0f , 178 . 0f ); context. line_to ( 69 . 0f , 208 . 0f ); context. line_to ( 80 . 0f , 143 . 0f ); context. line_to ( 32 . 0f , 97 . 0f ); context. line_to ( 98 . 0f , 87 . 0f ); context. close_path (); // Set up the drop shadow. context. set_shadow_blur ( 8 . 0f ); context. shadow_offset_y = 4 . 0f ; context. set_shadow_color ( 0 . 0f , 0 . 0f , 0 . 0f , 0 . 5f ); // Fill the star with yellow. context. set_color ( canvas_ity::fill_style, 1 . 0f , 0 . 9f , 0 . 2f , 1 . 0f ); context. fill (); // Draw the star with a thick red stroke and rounded points. context. line_join = canvas_ity::rounded; context. set_line_width ( 12 . 0f ); context. set_color ( canvas_ity::stroke_style, 0 . 9f , 0 . 0f , 0 . 5f , 1 . 0f ); context. stroke (); // Draw the star again with a dashed thinner orange stroke. float segments[] = { 21 . 0f , 9 . 0f , 1 . 0f , 9 . 0f , 7 . 0f , 9 . 0f , 1 . 0f , 9 . 0f }; context. set_line_dash ( segments, 8 ); context. line_dash_offset = 10 . 0f ; context. line_cap = canvas_ity::circle; context. set_line_width ( 6 . 0f ); context. set_color ( canvas_ity::stroke_style, 0 . 95f , 0 . 65f , 0 . 15f , 1 . 0f ); context. stroke (); // Turn off the drop shadow. context. set_shadow_color ( 0 . 0f , 0 . 0f , 0 . 0f , 0 . 0f ); // Add a shine layer over the star. context. set_linear_gradient ( canvas_ity::fill_style, 64 . 0f , 0 . 0f , 192 . 0f , 256 . 0f ); context. add_color_stop ( canvas_ity::fill_style, 0 . 30f , 1 . 0f , 1 . 0f , 1 . 0f , 0 . 0f ); context. add_color_stop ( canvas_ity::fill_style, 0 . 35f , 1 . 0f , 1 . 0f , 1 . 0f , 0 . 8f ); context. add_color_stop ( canvas_ity::fill_style, 0 . 45f , 1 . 0f , 1 . 0f , 1 . 0f , 0 . 8f ); context. add_color_stop ( canvas_ity::fill_style, 0 . 50f , 1 . 0f , 1 . 0f , 1 . 0f , 0 . 0f ); context. global_composite_operation = canvas_ity::source_atop; context. fill_rectangle ( 0 . 0f , 0 . 0f , 256 . 0f , 256 . 0f ); // Fetch the rendered RGBA pixels from the entire canvas. unsigned char *image = new unsigned char [ height * width * 4 ]; context. get_image_data ( image, width, height, width * 4 , 0 , 0 ); // Write them out to a TGA image file (TGA uses BGRA order). unsigned char header[] = { 0 , 0 , 2 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , width & 255 , width >> 8 , height & 255 , height >> 8 , 32 , 40 }; for ( int pixel = 0 ; pixel < height * width; ++pixel ) std::swap ( image[ pixel * 4 + 0 ], image[ pixel * 4 + 2 ] ); std::ofstream stream ( " example.tga " , std::ios::binary ); stream. write ( reinterpret_cast < char * >( header ), sizeof ( header ) ); stream. write ( reinterpret_cast < char * >( image ), height * width * 4 ); delete[] image; } < html > < head > < title > Example < body > < canvas id =" example " width =" 256 " height =" 256 " > < script type =" text/javascript " > const context = document . getElementById ( "example" ) . getContext ( "2d" ) ; // Build a star path. context . moveTo ( 128.0 , 28.0 ) ; context . lineTo ( 157.0 , 87.0 ) ; context . lineTo ( 223.0 , 97.0 ) ; context . lineTo ( 175.0 , 143.0 ) ; context . lineTo ( 186.0 , 208.0 ) ; context . lineTo ( 128.0 , 178.0 ) ; context . lineTo ( 69.0 , 208.0 ) ; context . lineTo ( 80.0 , 143.0 ) ; context . lineTo ( 32.0 , 97.0 ) ; context . lineTo ( 98.0 , 87.0 ) ; context . closePath ( ) ; // Set up the drop shadow. context . shadowBlur = 8.0 ; context . shadowOffsetY = 4.0 ; context . shadowColor = "rgba(0,0,0,0.5)" ; // Fill the star with yellow. context . fillStyle = "#ffe633" ; context . fill ( ) ; // Draw the star with a thick red stroke and rounded points. context . lineJoin = "round" ; context . lineWidth = 12.0 ; context . strokeStyle = "#e60080" ; context . stroke ( ) ; // Draw the star again with a dashed thinner orange stroke. const segments = [ 21.0 , 9.0 , 1.0 , 9.0 , 7.0 , 9.0 , 1.0 , 9.0 ] ; context . setLineDash ( segments ) ; context . lineDashOffset = 10.0 ; context . lineCap = "round" ; context . lineWidth = 6.0 ; context . strokeStyle = "#f2a626" ; context . stroke ( ) ; // Turn off the drop shadow. context . shadowColor = "rgba(0,0,0,0.0)" ; // Add a shine layer over the star. let gradient = context . createLinearGradient ( 64.0 , 0.0 , 192.0 , 256.0 ) ; gradient . addColorStop ( 0.30 , "rgba(255,255,255,0.0)" ) ; gradient . addColorStop ( 0.35 , "rgba(255,255,255,0.8)" ) ; gradient . addColorStop ( 0.45 , "rgba(255,255,255,0.8)" ) ; gradient . addColorStop ( 0.50 , "rgba(255,255,255,0.0)" ) ; context . fillStyle = gradient ; context . globalCompositeOperation = "source-atop" ; context . fillRect ( 0.0 , 0.0 , 256.0 , 256.0 ) ;

✨ Features

High-quality rendering

Trapezoidal area antialiasing provides very smooth antialiasing, even when lines are nearly horizontal or vertical.

Gamma-correct blending, interpolation, and resampling are used throughout. It linearizes all colors and premultiplies alpha on input and converts back to unpremultiplied sRGB on output. This reduces muddiness on many gradients (e.g., red to green), makes line thicknesses more perceptually uniform, and avoids dark fringes when interpolating opacity.

... continue reading