PHOBOSLAB

Blog Home

Ejecta

Ejecta is a fast JavaScript, Canvas & Audio implementation for iOS. Today, I'm releasing it under the MIT Open Source license.

Visit the Ejecta website for more info on what it is and how to use it. I will talk a bit more about some implementation details for the Canvas API here.

Implementing a general purpose drawing API, such as the HTML5 Canvas API, on top of OpenGL is by no means an easy endeavor. Before I decided to roll my own solution (you know, I have this problem), I looked at a number of graphic libraries including Google's skia and OpenVG.

I discovered exactly what I feared beforehand: these libraries do way too much, are too large and too hard to implement. You can't just use them here and there to draw – instead they replace your whole drawing stack. Getting them to compile alone is a huge pain; getting them to compile on the iPhone and then get them do what you wanted to seemed close to impossible.

So I began working on my own solution. Implementing the path methods for moveTo(), lineTo(), bezierCurveTo(), etc. was fairly straight forward: have an array of subpaths where each subpath is an array of points (x,y). Each call to the API methods pushes one or more points to the subpath or closes it.

However, I struggled a bit with getting bezier curves to behave in a manner that makes sense for the current scale; i.e. push more points for large bezier curves and at sharp corners, fewer points for smaller ones and straight lines. After a few days of reading and experimenting, I found this excellent article on adaptive bezier curves and adopted its solution.

The hard part was getting that array of points on the screen. For drawing lines (.stroke()) I didn't want to go with the obvious solution of just using GL_LINES, because it has a number of drawbacks, especially on iOS: no anti aliasing, limited line width and no miters or line caps.

So instead of using GL_LINES to draw, I ended up creating 2 triangles for each line segment and calculate the miter values myself. This correctly honors the APIs .miterLimit property, though the bevel it then draws is still a bit off. The code I ended up with is a bit on the ugly side, because it handles a lot of edge cases, but all in all this solution worked very well and is extremely fast.

Implementing .fill() proved to be yet another challenge. With OpenGL, before you can draw a primitive to the screen, you have to break it down into triangles first. This is quite easy to do for convex polygons, but not so much for concave ones that potentially have holes in them.

I spent a few days looking for triangulation library and soon realized that this is serious business. Triangle for instance, sports 16k loc – I'm quite allergic to libraries that need that much code to solve seemingly simple problems. Poly2Tri looked much more sane, but apparently has some stability problems.

After a bit of searching, I found libtess2, which is based on OpenGL's libtess and is supposed to be extremely robust and quite fast. The code base is excellent and I had no problem implementing it with Ejecta.

However, some tests showed that it's much slower than I hoped it would be. Realtime triangulation of complex polygons isn't very feasible on the iPhone.

In the end, I found a trick that lets you draw polygons in OpenGL without triangulating them first. It is so simple and elegant to implement, yet so ingenious: You can draw polygons with a simple triangle fan and mark those areas that you overdraw in the stencil buffer. See Drawing Filled, Concave Polygons Using the Stencil Buffer. It's a hacker's solution – thinking outside the box – and it fills me with joy.

There's still some parts missing in my Canvas implementation, namely gradients, shadows and most notably: text. I believe the best solution for drawing text in OpenGL, while honoring the Canvas spec, would be drawing to a texture using the iPhone's CG methods. This will make it quite slow, but should be good enough for a few paragraphs of text.

If you want to help out with anything grab the Ejecta source code on github – I'd be honored.

Wednesday, September 26th 2012

13 Comments:

#1 – Amadeus – Tuesday, September 25th 2012, 18:12

Dude, this is like Christmas! Thanks for all your hard work!

#2coopaq – Tuesday, September 25th 2012, 18:26

Someone please fork and start android port. iOS gets all the love. Would pay for iOS/Android support :)

#3 – oceddi – Tuesday, September 25th 2012, 18:31

This is seriously awesome stuff Dominic. Thank you so much for giving it to the world.

#4 – rianflo – Wednesday, September 26th 2012, 07:20

Nice job man and thank you! I love the approach.
I have one problem with an Impact game. It needs a hell lot of post-processing (pixel by pixel) and this is really slow. How fast is this implementations's get/putImageData?

#5Dominic – Wednesday, September 26th 2012, 14:28

get/putImageData is quite slow in Ejecta. I'm using normal JavaScript Arrays here, because the JSC API lacks support for ByteArrays. Maybe you could do your post-processing with a OpenGL pixel shader instead?

#6 – dd – Saturday, October 6th 2012, 02:22

I am happy I found ejecta library. Suits perfect for my need. I would be glad if I could contribute to project :-)

#7 – dd – Saturday, October 6th 2012, 02:22

Is it possible to display fps counter?

#8 – p3ga5e – Tuesday, October 9th 2012, 09:53

Nice Job !
did you plan to implement WebGL too ?

#9 – Nick – Sunday, October 14th 2012, 03:11

Nice work. The audio parts are somewhat unclear however. Let's say you add a file (mp3) in /music/song.mp3, presumably(?) you would play it like

var song = document.createElement("audio");
song.src="music/song.mp3"; //paths being relative to the root after all
song.load(); //I'm guessing here but this should synchronous right? the preload flag might be the backup
song.play();

Apparently I'm missing something as that doesn't work at all.

#10rezoner – Sunday, November 11th 2012, 10:20

Now it finally makes sense to write a canvas GUI library :)

#11Phil – Saturday, November 24th 2012, 22:48

Hey! Awesome project! I'm curious: you mention that CAAT and ThreeJS could be used easily with Ejecta, but I'm wondering if you see easy ways to get other libraries working? I'm thinking specifically of processing.js, since its scope is very similar to what can be done with native canvas+javascript, but it adds a number of slightly higher-level abstractions despite rendering content finally with only canvas methods. For example, beziers can not only be drawn, but geometric properties can be accessed as well, enabling a broad range of semi-CAD like uses for the products. Take a look at <a href="processingjs.org/reference/bezierTangent_/">this page</a>, for example. Support for a library like this one would come with a large dedicated community of supporters, too!

#12Phil – Sunday, November 25th 2012, 02:38

Alternately, paperJS might be a better fit. The problem with both of these libraries, though, is that they seem to rely on doing some small bits of DOM manipulation which, when included using require() in Ejecta, causes all kinds of problems. Do you have any experience with these issues when you tried threeJS or CAAT?

#13 – Chris – Wednesday, December 26th 2012, 03:51

I'm wondering if you did any experimentation of using Quartz 2D instead of OpenGL for parts of your work. Quartz 2D implements many of the required features for path rendering and so I'm wondering if it might be more efficient to use that API for drawing paths, gradients, text, etc. Take a look: developer.apple.com/library/mac/#documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/Introduction/Introduction.html

Post a Comment:

Comment: (Required)

(use <code> tags for preformatted text; URLs are recognized automatically)

Name: (Required)

URL:

Please type phoboslab into the following input field or enable Javascript. This is an anti-spam measure. Sorry for the inconvenience.