[HN Gopher] Canvas_ity: A tiny, single-header <canvas>-like 2D r...
___________________________________________________________________
Canvas_ity: A tiny, single-header <canvas>-like 2D rasterizer for
C++
Author : PaulHoule
Score : 114 points
Date : 2026-02-21 18:50 UTC (21 hours ago)
(HTM) web link (github.com)
(TXT) w3m dump (github.com)
| ranger_danger wrote:
| vibe-coded?
| nicoburns wrote:
| Most likely not seeing as the commit containing the bulk of the
| implementation dropped in 2022.
| ranger_danger wrote:
| maybe just the README then
| flowerbreeze wrote:
| The README is older than ChatGPT too. It's very unlikely
| that it's vibe coded or vibe written.
| a_e_k wrote:
| 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!
| peter-m80 wrote:
| Would that be an issue?
| 1bpp wrote:
| Yes.
| Amlal wrote:
| 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.
| ivanjermakov wrote:
| A lot of risks compared to what? I imagine bugs in kernel
| drivers or disk utilities be riskier.
| JoeyJoJoJr wrote:
| Such as?
| a_e_k wrote:
| 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.
| nicoburns wrote:
| The list of "recommended reading" from one of the issues looks
| great:
|
| https://github.com/a-e-k/canvas_ity/issues/11#issuecomment-2...
| lioeters wrote:
| Quoting the list here for visibility and archival purpose.
|
| * [Euclidean Vector - Properties and Operations](https://en.wik
| ipedia.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](h
| ttp://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/201
| 5/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](htt
| ps://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_C
| asteljau%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.sourcefor
| ge.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/libra
| ry/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 wrote:
| 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 th
|
| 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_i
| s_p...
| Lerc wrote:
| It would be interesting to compile to WASM to compare side by
| side for performance and accuracy.
| a_e_k wrote:
| 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.)
| jurschreuder wrote:
| 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.
| msephton wrote:
| 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.
| injidup wrote:
| 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.
| ddtaylor wrote:
| 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. #define CANVAS_ITY_IMPLEMENTATION
| socalgal2 wrote:
| 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
| pjmlp wrote:
| It is a common pattern among those that don't want to learn
| build systems, which isn't exactly the same.
| capisce wrote:
| Or among those who want to support any build system
| pjmlp wrote:
| We managed during the last 20 years just fine.
| socalgal2 wrote:
| 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.
| pjmlp wrote:
| Yeah, and?
|
| 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 wrote:
| 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?
| pjmlp wrote:
| I see a learning problem, because there is hardly a
| programming language in widespread use using a single
| one.
|
| 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 wrote:
| 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.
| ddtaylor wrote:
| 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?
| elteto wrote:
| Because not everyone is using Makefiles or CMake.
|
| 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 wrote:
| 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.
| PaulDavisThe1st wrote:
| 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.
| taminka wrote:
| i swear if someone starts another single header vs other options
| debate in this comment section i'm gonna explode
| pjmlp wrote:
| Boom! C and C++ aren't scripting languages.
| a_e_k wrote:
| 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://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 wrote:
| Thank You For Making And Sharing, a_e_k!
| Keyframe wrote:
| really nice! I'd prefer to see a C89 version though, however
| arcane you might think that is.
___________________________________________________________________
(page generated 2026-02-22 16:01 UTC)