* [W3C HTML5 2D canvas specification](https://www.w3.org/TR/2015/REC-2dcontext-20151119/) - Obviously, this provides the basis that I was trying to adapt closely to a C++ API. There's a lot of detail in here, including some abstract descriptions of how the implementations are supposed to work.
* [Circles in Angles](http://eastfarthing.com/blog/2018-12-27-circle/) - This was a blog post that I wrote after working out the math for how to calculate where to put the center of a circle of a given radius that is inscribed in an angle. This is needed for the `arc_to()` method, but also for computing miter joins for lines.
* [Drawing an Elliptical Arc Using Polylines, Quadratic or Cubic Bezier Curves](https://web.archive.org/web/20210414175418/https://www.space...) - In my implementation, all shapes get lowered to a series of cubic Bezier splines. In most cases the conversion is exact, but in the case of circular arcs it's approximate. I used this reference for the original implementation for `arc()`. I later changed to a slightly simpler home-grown solution that was more accurate for my needs, but this was a good start.
* [Converting Stroked Primitives to Filled Primitives](https://w3.impa.br/~diego/publications/Neh20.pdf), [Polar Stroking: New Theory and Methods for Stroking Paths](https://developer.download.nvidia.com/video/siggraph/2020/pr...) - This pair of papers dealing with modern stroke expansion were published concurrently at SIGGRAPH 2020 and were very useful background when I was writing the stroke expansion code. They both have some great examples of how naive methods can fail in high-curvature cases, and how it can be done correctly. I didn't use the full-fat version of these, but I did borrow some ideas (especially from Fig. 10 of the first) without trying to be clever about simplify the path. I also borrowed some of the nice test cases to make sure my code handled them correctly. (It's surprising how many browser canvas implementations don't.) It's also worth learning something about Gaussian curvature, if you don't know it already; both papers give some background on that.
* [De Casteljau's Algorithm](https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm) - I use recursive tesselation for flattening cubic Bezier splines to a series of line segments (forming polygons). De Casteljau's algorithm is the basis of this, where it recursively splits Bezier splines in half by computing series of midpoints.
* [Adaptive Subdivision of Bezier Curves](https://agg.sourceforge.net/antigrain.com/research/adaptive_...) - This is a nice writeup by the late author of Anti-Grain Geometry that goes into more details of the recursion, with some ideas about choosing where to split. Adaptive subdivision methods choose whether to recurse or stop based on some estimate of error. I don't use the exact approach here, but a conservative estimate of the maximum distance from the curve, plus a maximum angular turn (determined by solving for the [sagitta](https://en.wikipedia.org/wiki/Sagitta_(geometry)) so that stroke expansion from the tessellated line segments is of sufficient quality).
* [Reentrant Polygon Clipping](https://dl.acm.org/doi/pdf/10.1145/360767.360802) - While I could just rasterize the entire set of polygons and skip over any pixels outside the screen window (and I did exactly this for a large part of the development), it's a lot more efficient to clip the polygons to the screen window first. Then rasterizing only worries about what's visible. I used the classic Sutherland-Hodgman algorithm for this.
* [How the stb_truetype Anti-Aliased Software Rasterizer v2 Works](https://nothings.org/gamedev/rasterize/) - I drew inspiration for this for rasterization with signed trapezoidal areas, but implemented the trapezoidal area idea rather differently than this. Still, this should give you an idea for at least one way of doing it.
* [Physically Based Rendering (4th Ed), Chapter 8, Sampling and Reconstruction](https://pbr-book.org/4ed/Sampling_and_Reconstruction) - This is stuff I already knew very well from my day job at the time writing 3D renderers, but the stuff here, especially Section 8.1, is useful background on how to resample an image correctly. I used this kind of approach to do high quality resampling images for pattern fills and for the `draw_image()` method.
* [Cubic Convolution Interpolation for Digital Image Processing](https://ncorr.com/download/publications/keysbicubic.pdf) - When you hear of "bicubic interpolation" in an image processing or picture editing program, it's usually the kernel from this paper. This is the specific kernel that I used for the resampling code. It smoothly interpolates with less blockiness that bilinear interpolation when magnifying, and it's a piece-wise polynomial approximation to the sinc function so it antialiases well to when minifying.
* [Theoretical Foundations of Gaussian Convolution by Extended Box Filtering](https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11....) - Naive Gaussian blurring (for soft drop shadows here) can be slow when the blur radius is large, since each pixel will need to be convolved with a large Gaussian kernel. It's separable, so instead of doing full 2D convolutions, you can do a pass of 1D convolutions on all the rows, then all the columns or vice versa. But that's still slow. However, iterated convolution of a box kernel is a very good approximation (think of summing dice approaching a Gaussian distribution). And box blurring is very fast, regardless of the kernel size since everything has the same weight - you just add to and subtract from a running sum. This paper is about quickly approximating Gaussian blurs with iterated box-like blurs.
* [Compositing Digital Images](https://graphics.pixar.com/library/Compositing/paper.pdf) - Porter-Duff forms the basis for the core compositing and blend modes in vector graphics, and is referenced directly by the Canvas spec. For my implementation, I break down the choices of the parameters to use into four bits and encode them directly into the enum of the operation. That way I can implement all the Porter-Duff operations in just 7 lines of code. (I'm pretty proud of that!)
* [sRGB](https://en.wikipedia.org/wiki/SRGB) - The Canvas spec - transitively, via reference to the [CSS color spec](https://www.w3.org/TR/css-color-3/) - defines that input colors are in sRGB. While many vector graphics implementations compute in sRGB directly, operating in linearized RGB is a hill I'll die on. (I don't go crazy about color spaces beyond that, though.) If you don't you'll end up with odd looking gradients, inconsistent appearance of antialiased thin line widths and text weights, different text weights for light-on-dark vs. dark-on-light, color shifts when resizing. [Here are some examples](https://blog.johnnovak.net/2016/09/21/what-every-coder-shoul...). I do all my processing and storage in linear RGB internally and convert to and from sRGB on input and output.
* [GPUs prefer premultiplication](https://www.realtimerendering.com/blog/gpus-prefer-premultip...) - Premultiplied alpha is also important for correct-looking blending. The Canvas spec actually dictates _non_-premultiplied alpha, so this is another case where I convert to premultiplied alpha on input, do everything with premultiplied alpha internally, and then un-premultiply on output.
* [Dithering](https://en.wikipedia.org/wiki/Dither) - I use floating point RGB color internally and convert and quantize to 8-bit sRGB on output. That means that the internal image buffer can easily represent subtle gradients, but the output may easily end up banded if there are too few steps in the 8-bit sRGB space. My library applies [ordered dithering](https://en.wikipedia.org/wiki/Ordered_dithering) to its output to prevent the banding.
Author here. What a pleasant surprise to see this trending on the front page!
(I did post a Show HN at the time of the original release, https://github.com/nothings/stb) and by libraries inspired by those, all of which I've found very convenient. In particular, I like their convenience for small utilities written in a single .cpp file where I can just `g++ -O3 -o prog prog.cpp` or such to compile without even bothering with a makefile or CMake.
Since the implementation here is all within a single #ifdef block, I had figured that anyone who truly preferred separate .cpp and .h files could easily split it themselves in just a few minutes.
But anyway, I thought this would be a fun way of "giving back" to the STB header ecosystem and filling what looked to me like an obvious gap among the available header libraries. It started as something that I'd wished I'd had before, for doing some lightweight drawing on top of images, and it just kind of grew from there. (Yes, there was Skia and Cairo, but both seemed way heavier weight than they ought to be, and even just building Skia was an annoying chore.)
----
Since I mentioned a v2.0, I do have a roadmap in mind with a few things for it: beside the small upgrades mentioned in the GitHub issues to support parts of newer <canvas> API specs (alternate fill rules, conic gradients, elliptical arcs, round rectangles) and text kerning, I'm thinking about porting it to a newer C++ standard such as C++20 (I intentionally limited v1.0 to C++03 so that it could be used in as many places as possible), possibly including a small optional library on top of it to parse and rasterize a subset of SVG, and an optional Python binding.
The project is great. The HN comments are embarrassing. Isn’t it ironic to imply laziness by chiming in with “vibe coded” which in itself is such a lazy reaction.
And thus random 2D drawing APIs begat Cairo, and then Cairo begat the Canvas, and thus the Canvas begat Canvas_ity, which looked almost like it's grandparent, and yet was very much it's own self.
Author here. I have a JavaScript port of my automated test suite (https://github.com/a-e-k/canvas_ity/blob/main/test/test.html) that I used to compare my library against browser <canvas> implementations. I was surprised by all of the browser quirks that I found!
But compiling to WASM and running side-by-side on that page is definitely something that I've thought about to make the comparison easier. (For now, I just have my test suite write out PNGs and compare them in an image viewer split-screen with the browser.)
It is common for header-only libraries: you need to include this header in one c++ using the macro for linking (don't use that macro in other c++ files to avoid duplicate symbols). In C++, you can declare a function as many times as you want, but you can only define it (write the actual body) once in the entire project.
I understand that part, but I don't see why do this instead of basic Makefile or CMake setup. It seems like more work than a regular linker at that point. For what purpose?
The list of "recommended reading" from one of the issues looks great:
https://github.com/a-e-k/canvas_ity/issues/11#issuecomment-2...
Quoting the list here for visibility and archival purpose.
* [Euclidean Vector - Properties and Operations](https://en.wikipedia.org/wiki/Euclidean_vector#Properties_an...) - I assume you know all this already since it's pretty fundamental, but just in case, you'll want to be really comfortable with 2D vector math, and the [dot product](https://en.wikipedia.org/wiki/Dot_product) especially. In 2D graphics, I also find uses for the ["perp dot" product](http://cas.xav.free.fr/Graphics%20Gems%204%20-%20Paul%20S.%2...) all the time. (I maintain that in graphics, if you're calling trig functions too much, you're probably doing it wrong!)
* [W3C HTML5 2D canvas specification](https://www.w3.org/TR/2015/REC-2dcontext-20151119/) - Obviously, this provides the basis that I was trying to adapt closely to a C++ API. There's a lot of detail in here, including some abstract descriptions of how the implementations are supposed to work.
* [ISO Open Font Format spec](http://wikil.lwwhome.cn:28080/wp-content/uploads/2018/06/ISO...), [Apple TrueType Reference Manual](https://developer.apple.com/fonts/TrueType-Reference-Manual/), [Microsoft OpenType Spec](https://learn.microsoft.com/en-us/typography/opentype/spec/) - These are the references that I consulted when it came to adding font and text support. In particular, the descriptions of the internal tables where useful when I was writing the code to parse TrueType font files.
* [Circles in Angles](http://eastfarthing.com/blog/2018-12-27-circle/) - This was a blog post that I wrote after working out the math for how to calculate where to put the center of a circle of a given radius that is inscribed in an angle. This is needed for the `arc_to()` method, but also for computing miter joins for lines.
* [Drawing an Elliptical Arc Using Polylines, Quadratic or Cubic Bezier Curves](https://web.archive.org/web/20210414175418/https://www.space...) - In my implementation, all shapes get lowered to a series of cubic Bezier splines. In most cases the conversion is exact, but in the case of circular arcs it's approximate. I used this reference for the original implementation for `arc()`. I later changed to a slightly simpler home-grown solution that was more accurate for my needs, but this was a good start.
* [Converting Stroked Primitives to Filled Primitives](https://w3.impa.br/~diego/publications/Neh20.pdf), [Polar Stroking: New Theory and Methods for Stroking Paths](https://developer.download.nvidia.com/video/siggraph/2020/pr...) - This pair of papers dealing with modern stroke expansion were published concurrently at SIGGRAPH 2020 and were very useful background when I was writing the stroke expansion code. They both have some great examples of how naive methods can fail in high-curvature cases, and how it can be done correctly. I didn't use the full-fat version of these, but I did borrow some ideas (especially from Fig. 10 of the first) without trying to be clever about simplify the path. I also borrowed some of the nice test cases to make sure my code handled them correctly. (It's surprising how many browser canvas implementations don't.) It's also worth learning something about Gaussian curvature, if you don't know it already; both papers give some background on that.
* [De Casteljau's Algorithm](https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm) - I use recursive tesselation for flattening cubic Bezier splines to a series of line segments (forming polygons). De Casteljau's algorithm is the basis of this, where it recursively splits Bezier splines in half by computing series of midpoints.
* [Adaptive Subdivision of Bezier Curves](https://agg.sourceforge.net/antigrain.com/research/adaptive_...) - This is a nice writeup by the late author of Anti-Grain Geometry that goes into more details of the recursion, with some ideas about choosing where to split. Adaptive subdivision methods choose whether to recurse or stop based on some estimate of error. I don't use the exact approach here, but a conservative estimate of the maximum distance from the curve, plus a maximum angular turn (determined by solving for the [sagitta](https://en.wikipedia.org/wiki/Sagitta_(geometry)) so that stroke expansion from the tessellated line segments is of sufficient quality).
* [Reentrant Polygon Clipping](https://dl.acm.org/doi/pdf/10.1145/360767.360802) - While I could just rasterize the entire set of polygons and skip over any pixels outside the screen window (and I did exactly this for a large part of the development), it's a lot more efficient to clip the polygons to the screen window first. Then rasterizing only worries about what's visible. I used the classic Sutherland-Hodgman algorithm for this.
* [How the stb_truetype Anti-Aliased Software Rasterizer v2 Works](https://nothings.org/gamedev/rasterize/) - I drew inspiration for this for rasterization with signed trapezoidal areas, but implemented the trapezoidal area idea rather differently than this. Still, this should give you an idea for at least one way of doing it.
* [Physically Based Rendering (4th Ed), Chapter 8, Sampling and Reconstruction](https://pbr-book.org/4ed/Sampling_and_Reconstruction) - This is stuff I already knew very well from my day job at the time writing 3D renderers, but the stuff here, especially Section 8.1, is useful background on how to resample an image correctly. I used this kind of approach to do high quality resampling images for pattern fills and for the `draw_image()` method.
* [Cubic Convolution Interpolation for Digital Image Processing](https://ncorr.com/download/publications/keysbicubic.pdf) - When you hear of "bicubic interpolation" in an image processing or picture editing program, it's usually the kernel from this paper. This is the specific kernel that I used for the resampling code. It smoothly interpolates with less blockiness that bilinear interpolation when magnifying, and it's a piece-wise polynomial approximation to the sinc function so it antialiases well to when minifying.
* [Theoretical Foundations of Gaussian Convolution by Extended Box Filtering](https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11....) - Naive Gaussian blurring (for soft drop shadows here) can be slow when the blur radius is large, since each pixel will need to be convolved with a large Gaussian kernel. It's separable, so instead of doing full 2D convolutions, you can do a pass of 1D convolutions on all the rows, then all the columns or vice versa. But that's still slow. However, iterated convolution of a box kernel is a very good approximation (think of summing dice approaching a Gaussian distribution). And box blurring is very fast, regardless of the kernel size since everything has the same weight - you just add to and subtract from a running sum. This paper is about quickly approximating Gaussian blurs with iterated box-like blurs.
* [Compositing Digital Images](https://graphics.pixar.com/library/Compositing/paper.pdf) - Porter-Duff forms the basis for the core compositing and blend modes in vector graphics, and is referenced directly by the Canvas spec. For my implementation, I break down the choices of the parameters to use into four bits and encode them directly into the enum of the operation. That way I can implement all the Porter-Duff operations in just 7 lines of code. (I'm pretty proud of that!)
* [sRGB](https://en.wikipedia.org/wiki/SRGB) - The Canvas spec - transitively, via reference to the [CSS color spec](https://www.w3.org/TR/css-color-3/) - defines that input colors are in sRGB. While many vector graphics implementations compute in sRGB directly, operating in linearized RGB is a hill I'll die on. (I don't go crazy about color spaces beyond that, though.) If you don't you'll end up with odd looking gradients, inconsistent appearance of antialiased thin line widths and text weights, different text weights for light-on-dark vs. dark-on-light, color shifts when resizing. [Here are some examples](https://blog.johnnovak.net/2016/09/21/what-every-coder-shoul...). I do all my processing and storage in linear RGB internally and convert to and from sRGB on input and output.
* [GPUs prefer premultiplication](https://www.realtimerendering.com/blog/gpus-prefer-premultip...) - Premultiplied alpha is also important for correct-looking blending. The Canvas spec actually dictates _non_-premultiplied alpha, so this is another case where I convert to premultiplied alpha on input, do everything with premultiplied alpha internally, and then un-premultiply on output.
* [Dithering](https://en.wikipedia.org/wiki/Dither) - I use floating point RGB color internally and convert and quantize to 8-bit sRGB on output. That means that the internal image buffer can easily represent subtle gradients, but the output may easily end up banded if there are too few steps in the 8-bit sRGB space. My library applies [ordered dithering](https://en.wikipedia.org/wiki/Ordered_dithering) to its output to prevent the banding.
Author here. What a pleasant surprise to see this trending on the front page!
(I did post a Show HN at the time of the original release, https://github.com/nothings/stb) and by libraries inspired by those, all of which I've found very convenient. In particular, I like their convenience for small utilities written in a single .cpp file where I can just `g++ -O3 -o prog prog.cpp` or such to compile without even bothering with a makefile or CMake.
Since the implementation here is all within a single #ifdef block, I had figured that anyone who truly preferred separate .cpp and .h files could easily split it themselves in just a few minutes.
But anyway, I thought this would be a fun way of "giving back" to the STB header ecosystem and filling what looked to me like an obvious gap among the available header libraries. It started as something that I'd wished I'd had before, for doing some lightweight drawing on top of images, and it just kind of grew from there. (Yes, there was Skia and Cairo, but both seemed way heavier weight than they ought to be, and even just building Skia was an annoying chore.)
----
Since I mentioned a v2.0, I do have a roadmap in mind with a few things for it: beside the small upgrades mentioned in the GitHub issues to support parts of newer <canvas> API specs (alternate fill rules, conic gradients, elliptical arcs, round rectangles) and text kerning, I'm thinking about porting it to a newer C++ standard such as C++20 (I intentionally limited v1.0 to C++03 so that it could be used in as many places as possible), possibly including a small optional library on top of it to parse and rasterize a subset of SVG, and an optional Python binding.
The project is great. The HN comments are embarrassing. Isn’t it ironic to imply laziness by chiming in with “vibe coded” which in itself is such a lazy reaction.
And thus random 2D drawing APIs begat Cairo, and then Cairo begat the Canvas, and thus the Canvas begat Canvas_ity, which looked almost like it's grandparent, and yet was very much it's own self.
It would be interesting to compile to WASM to compare side by side for performance and accuracy.
Author here. I have a JavaScript port of my automated test suite (https://github.com/a-e-k/canvas_ity/blob/main/test/test.html) that I used to compare my library against browser <canvas> implementations. I was surprised by all of the browser quirks that I found!
But compiling to WASM and running side-by-side on that page is definitely something that I've thought about to make the comparison easier. (For now, I just have my test suite write out PNGs and compare them in an image viewer split-screen with the browser.)
Wow very nice work I really like it!
Very clean :) I will use it!
We made our own OpenCV alternative at Kexxu I'll put it in :) exactly what it still needed for a bit of basic drawing.
Thank you for sharing. The only thing I don't understand why this is a header only implementation with a macro that goes in a C++ file.
It is common for header-only libraries: you need to include this header in one c++ using the macro for linking (don't use that macro in other c++ files to avoid duplicate symbols). In C++, you can declare a function as many times as you want, but you can only define it (write the actual body) once in the entire project.
I understand that part, but I don't see why do this instead of basic Makefile or CMake setup. It seems like more work than a regular linker at that point. For what purpose?
2 replies →
that's a common pattern in C++ land because there is no standard way to use libraries in C++
https://github.com/p-ranav/awesome-hpp
It is a common pattern among those that don't want to learn build systems, which isn't exactly the same.
1 reply →
Thank You For Making And Sharing, a_e_k!
i swear if someone starts another single header vs other options debate in this comment section i'm gonna explode
Boom! C and C++ aren't scripting languages.
vibe-coded?
Most likely not seeing as the commit containing the bulk of the implementation dropped in 2022.
maybe just the README then
2 replies →
Would that be an issue?
Yes, it's a canvas library, there's a lot of risks of including AI generated code that hasn't been checked in a rasterizing library.
3 replies →
Yes.