Author here. No vibe-coding, all human-written. Are you thinking of my use of GitHub emoji on the section headings in the README? I just found they helped my eye pick out the headings a little more easily and I'd seen some other nice READMEs at the time do that sort of thing when I went looking for examples to pattern it off of. I swear I'd had no idea it would become an LLM thing!
Author here. There's no AI-generated code in this. But yes, security hardening this has not been a priority of mine (though I do have some ideas about fuzz testing it), so for now - like with many small libraries of this nature - it's convenient but best used only with trusted inputs if that's a concern.
* [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.
I found the "perp dot product" an interesting one. It's a pity the description is in a massive pdf (though it looks like a great book). The top Google result is the MathWorld page [1] but it's very brief.
Here how that pdf describes it. It first defines the perpendicular operator on a 2D vector x as
x⟂ := (-x_2, x_1)
which is x rotated 90 degrees anticlockwise. Then the perp dot product of two 2D vectors is defined as
x⟂ . y
This has a few interesting properties, most notably that
x⟂ . y = |x| |y| sin θ
For example, the sign of the perp dot product tells you whether you need to rotate clockwise or anticlockwise to get from x to y. If it's zero then they're parallel – could be pointing in same or opposite directions (or over or both are zero).
In this Reddit post [2] about it, again not much is said, but a redditor makes the astute observation:
> The perp dot product is the same as the cross product of vectors in a plane, except that you take the magnitude of the z component and ignore the x/y components (which are 0).
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.)
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.
I remember a old grey beard calling me lazy because I programmed in C++ instead of assembler. Using LLMs has pushed this attitude up a few abstraction layers.
In my experience. I've run into the issue quite often. You find some library, it has its own build system (meaning not the one you're using). It has special rules etc... Integrating into your build system is time consuming and frustrating. Compiler errors from includes, linker errors, etc..
None of that happens with a single file C++ library.
And what build system do you recommend the entire ecosystem support? Well, I choose (arbitrarily different and incompatible one to prove a point). Do you see the problem?
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?
We can also argue C++ is not a scripting language, which is what is approach is all about.
When C and C++ were the main programming languages during the 1990's, and commercial compilers abounded strangely we could manage handling all those build systems approaches.
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.
I'd originally started with a different version control system and was still getting used to Git and GitHub at the time that I'd released this. (I was a latecomer to Git just because I hated the CLI so much.) It was easiest for me just to drop the whole thing as a snapshot in a single commit.
But my private repo for it actually started in May 2017, and it had 320 commits leading up to its release, all human-written.
For the v2.0 that I have in mind, I'm thinking of force-pushing to migrate the full development history to the public repo.
And finally I'll add that I'm a graphics engineer by education and career. Where would the fun be in vibe-coding this? :-) Oh, and this compiles down to just ~36KiB of object code on x86-64 last I checked. Good luck vibe-coding that constraint.
2. Why a single header with `#define CANVAS_ITY_IMPLEMENTATION`?
I was inspired by the STB header ibraries (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.
ranger_danger | a day ago
nicoburns | a day ago
ranger_danger | a day ago
flowerbreeze | a day ago
a_e_k | 20 hours ago
peter-m80 | a day ago
1bpp | a day ago
Amlal | a day ago
ivanjermakov | a day ago
JoeyJoJoJr | a day ago
a_e_k | 20 hours ago
nicoburns | a day ago
https://github.com/a-e-k/canvas_ity/issues/11#issuecomment-2...
lioeters | 17 hours ago
* [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.
quietbritishjim | 12 hours ago
Here how that pdf describes it. It first defines the perpendicular operator on a 2D vector x as
which is x rotated 90 degrees anticlockwise. Then the perp dot product of two 2D vectors is defined as This has a few interesting properties, most notably that For example, the sign of the perp dot product tells you whether you need to rotate clockwise or anticlockwise to get from x to y. If it's zero then they're parallel – could be pointing in same or opposite directions (or over or both are zero).In this Reddit post [2] about it, again not much is said, but a redditor makes the astute observation:
> The perp dot product is the same as the cross product of vectors in a plane, except that you take the magnitude of the z component and ignore the x/y components (which are 0).
[1] http://mathworld.wolfram.com/PerpDotProduct.html
[2] https://www.reddit.com/r/learnmath/comments/agfm8g/what_is_p...
Lerc | a day ago
a_e_k | 20 hours ago
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.)
jurschreuder | 14 hours ago
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.
msephton | a day ago
injidup | 13 hours ago
ddtaylor | a day ago
socalgal2 | a day ago
https://github.com/p-ranav/awesome-hpp
pjmlp | 15 hours ago
capisce | 15 hours ago
pjmlp | 14 hours ago
socalgal2 | 14 hours ago
None of that happens with a single file C++ library.
pjmlp | 14 hours ago
There isn't a single build system without issues, other than siloed languages without language standards.
Header libraries only started to be a thing when scripting generation educated in Python and Ruby during the 2010's turned into compiled languages.
chris37879 | 7 hours ago
pjmlp | 7 hours ago
And if you're going to point out Go or Rust, it kind of works as long, nothing else is used, and they don't need to interact with platform SDKs.
erwincoumans | 23 hours ago
ddtaylor | 23 hours ago
elteto | 23 hours ago
A true header-only library should be build-system agnostic and this is one way to do that.
We can argue about build systems for C++ all day long and never come to an agreement. With this approach this piece of code can be used anywhere.
pjmlp | 15 hours ago
When C and C++ were the main programming languages during the 1990's, and commercial compilers abounded strangely we could manage handling all those build systems approaches.
PaulDavisThe1st | 22 hours ago
taminka | 22 hours ago
pjmlp | 15 hours ago
a_e_k | 20 hours ago
(I did post a Show HN at the time of the original release, https://news.ycombinator.com/item?id=33148540, but it never gained traction.)
Just to answer some comments that I see:
1. This was absolutely not vibecoded!
I'd originally started with a different version control system and was still getting used to Git and GitHub at the time that I'd released this. (I was a latecomer to Git just because I hated the CLI so much.) It was easiest for me just to drop the whole thing as a snapshot in a single commit.
But my private repo for it actually started in May 2017, and it had 320 commits leading up to its release, all human-written.
For the v2.0 that I have in mind, I'm thinking of force-pushing to migrate the full development history to the public repo.
And finally I'll add that I'm a graphics engineer by education and career. Where would the fun be in vibe-coding this? :-) Oh, and this compiles down to just ~36KiB of object code on x86-64 last I checked. Good luck vibe-coding that constraint.
2. Why a single header with `#define CANVAS_ITY_IMPLEMENTATION`?
I was inspired by the STB header ibraries (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.
disqard | 17 hours ago
Keyframe | 7 hours ago