art with code

2009-11-28

JavaScript drawing apps

Wrote two small drawing apps during the last two days:

WebGL cube paint - draw with fancy 3D cubes.




MiniPaint - a 2D canvas drawing app in 25 lines.



The 25-line app has Wacom support (pressure, device type, tilt). Too bad that the only copy of Firefox with Wacom support is mine. (Yes, created bugs in Bugzilla and sent patches. Wait and see. BTW, if you know how to add Wacom support for Windows and Mac, that'd be awesome.)

2009-11-23

WebGL cubes

Started writing a small scene graph last weekend. Here cube shots from test demo:




I wrote and optimized one use-case for it as well! "Create bouncing cube on canvas (with lighting etc.)"

Ended up with the special-case-rife DWIM snippet: new Scene($('gl_canvas'), new Cube().makeBounce()).

Demos: lots of cubes and a simple bouncing cube.

2009-11-16

Further optimizing a small JavaScript loop

Continuing from Optimizing a small JavaScript loop, there were some suggestions in the comments on how it might be made faster (or cleaner.) I tested them all just now, along with a couple ideas of my own.

Using var l = imgs.length; while (l--) instead of a for-loop


No measurable effect on the speed of my loop, except that I had to reverse a sort order to account for the reverse traversal. And I don't like reversing sort orders, it's hard enough to keep these things straight when you're not counting backwards.

Not copying function args to local vars


I.e. instead of the redundant var ctop = arg2, use arg2 directly. Made the code cleaner. Didn't have any measurable effect on runtime.

Using object properties directly instead of copying them to local vars


I.e. instead of var etop = e.cachedTop use e.cachedTop directly. Made the code cleaner, no effect on runtime.

Using imgs[i] everywhere instead of var e = imgs[i]


That seems slower to me. And more typing. But, no measurable effect on runtime.

Using a > b ? a : b instead of Math.max(a,b), ditto for Math.min


This was one of my ideas. No measurable effect on runtime.

Inlining isVisible to the loop


Again, one of my ideas. Again, no measurable effect on runtime. Maybe a 5-10% improvement, but my measurement error is 10-20% :|

Moving setting e.trans outside the loop


Sounds like a good idea. Need to refactor my code to do that.

Conclusions


Most of the things people tell you will improve JavaScript performance actually don't. Measure.

Also, I need a better way to measure runtimes. What I have now is a keydown handler that runs the loop a hundred times and prints the elapsed time to Firebug console. The amusing thing is that the exact same code can give 100-run runtimes ranging from 550 ms to 720 ms. And my system load is zero.

I suppose the problem is that this benchmark is run as part of my existing application. So, if any enterprising soul wants to have a go, make the bit you benchmark a separate page.

2009-11-15

Product Activation, once again

Much the same way as it happened with XP, installing new drivers (= flashing the BIOS) on (^W^)7 broke the existing product activation. And much the same way as it happened with XP, my reaction is to write the Windows disk over with zeroes and replace it with something more useful.

After I finish playing Torchlight, that is. Maybe it runs on Wine...

2009-11-14

Optimizing a small JavaScript loop

I recently had to optimize a visibility-checking loop, as it was causing some slowness when dealing with several hundreds of images. Executive summary: profile with Firebug, cache regex results and unchanging DOM properties.

I started with a loop like this:

imgs.forEach(function(e) {
if (isVisible(e)) {
if (e.src.match(/\/transparent\.gif$/)) {
e.src = e.getAttribute('origsrc');
}
} else if (!e.src.match(/\/transparent\.gif$/)) {
e.src = '/transparent.gif';
}
});

isVisible = function(e) {
var ctop = window.scrollY;
var cbottom = ctop + window.innerHeight;
var etop = elem.offsetTop;
var ebottom = etop + elem.offsetHeight;
var top = Math.max(ctop, etop);
var bottom = Math.min(cbottom, ebottom);
return (bottom - top) > -1200;
}

Runtime for 400 images: 35ms. Gah!

Profile says that a lot of time is spent in isVisible. But there's really nothing there that looks expensive. Let's try caching window.scrollY and window.innerHeight anyhow.

var wsy = window.scrollY;
var wih = window.innerHeight;

imgs.forEach(function(e) {
if (isVisible(e, wsy, wih)) {
...

isVisible = function(e, wsy, wih) {
var ctop = wsy;
var cbottom = ctop + wih;
...

Runtime came down to 23 ms. Whoah, that's a third off the execution time. Maybe property access is super expensive, let's see if caching elem.offsetTop and elem.offsetHeight would help as well.

isVisible = function(e, wsy, wih) {
...
if (e.cachedTop == null || !e.complete) {
e.cachedTop = e.offsetTop;
e.cachedBottom = e.cachedTop + e.offsetHeight;
}
var etop = e.cachedTop
var ebottom = e.cachedBottom
...
}

Runtime down to 15 ms. Excellent.

Profile says that the forEach loop is using a lot of time. Maybe it's the regexes. How about using RegExp.test instead of match. The docs say it should be faster.

if ((/\/transparent\.gif$/).test(e.src)) {
...
} else if (!(/\/transparent\.gif$/).test(e.src)) {


Runtime down to 12 ms. RegExp.test is indeed faster than String.match. Next, we could do away with the regexp altogether as we control which images are transparent and which are not...


imgs.forEach(function(e) {
if (e.trans == null)
e.trans = (/\/transparent\.gif$/).test(e.src);
if (isVisible(e, wsy, wih)) {
if (e.trans) {
e.src = e.getAttribute('origsrc');
e.trans = false;
e.cachedTop = e.cachedBottom = null;
}
} else if (!e.trans) {
e.src = '/transparent.gif';
e.trans = true;
e.cachedTop = e.cachedBottom = null;
}
});

Okay, now we're down to 5 ms. Finally, let's try replacing the forEach-loop with a plain for-loop.

for (var i=0; i<imgs.length; i++) {
var e = imgs[i];
...
}

Down to 3 ms. Not all that much in absolute terms, but relative to 5 ms it's 40% less. I wonder if CRAZY MICRO-OPTIMIZATIONS would do anything.

for (var i=0, l=imgs.length, e=imgs[0]; i<l; e=imgs[++i]) {
...
}

Down to ... 3 ms. No change. Well, it might be 0.3 ms faster but my measurements have a larger error than that. Ditch the micro-optimizations for readability.

And 'lo! The final code that is 10x faster than what we started with:

var wsy = window.scrollY;
var wih = window.innerHeight;

for (var i=0; i<imgs.length; i++) {
var e = imgs[i];
if (e.trans == null)
e.trans = (/\/transparent\.gif$/).test(e.src);
if (isVisible(e, wsy, wih)) {
if (e.trans) {
e.src = e.getAttribute('origsrc');
e.trans = false;
e.cachedTop = e.cachedBottom = null;
}
} else if (!e.trans) {
e.src = '/transparent.gif';
e.trans = true;
e.cachedTop = e.cachedBottom = null;
}
}

isVisible = function(e, wsy, wih) {
var ctop = wsy;
var cbottom = ctop + wih;
if (e.cachedTop == null || !e.complete) {
e.cachedTop = e.offsetTop;
e.cachedBottom = e.cachedTop + e.offsetHeight;
}
var etop = e.cachedTop
var ebottom = e.cachedBottom
var top = Math.max(ctop, etop);
var bottom = Math.min(cbottom, ebottom);
return (bottom - top) > -1200;
}

Does it work right? It seems to, at least. So maybe! Or maybe not! Isn't caching fun?

[Edit] The offsetTop and offsetHeight caching didn't work on my case, so I had to remove them. Final runtime 11 ms, or only 3x faster. Maybe some day I figure out why it didn't work...

[Edit] People have suggested using var l = imgs.length; while (l--) { ... }, which not only looks odd and reverses the iteration order, but also generates the same code as a for-loop would (on WebKit at least), according to Oliver Hunt. Well, the codegen test is for var i=0; while (i < imgs.length) i++; but i assume it also applies for for (var l=imgs.length; l--;).

[Edit] It's also somewhat strange to see suggestions that focus on optimizing all these weird things like "use object properties directly instead of local variables", instead of the possibly big things: inline isVisible, replace Math.max and Math.min with a > b ? a : b and a < b ? a : b. Well. I'll try them all and post a follow-up. Stay tuned!

[Edit] Follow-up.

2009-11-11

Canvas-generated hexa background


Wrote that for the new fancy Metatunnel page (while procrastinating on writing an essay...) Here's the JS to boggle over:

function polygon(n) {
var a = [];
for (var i=0; i<n; i++)
a.push({x:Math.cos(2*Math.PI*(i/n)), y:Math.sin(2*Math.PI*(i/n))});
return a;
}
function id(i){ return i }
function lineTo(ctx){return function (p){ ctx.lineTo(p.x, p.y) }}
function linePath(ctx, path){ path.map(lineTo(ctx)) }
function drawPath(ctx, path, close){
ctx.beginPath(); linePath(ctx, path); if (close) ctx.closePath() }
function strokePath(ctx, path, close){ drawPath(ctx, path, true); ctx.stroke() }
function fillPath(ctx, path){ drawPath(ctx, path); ctx.fill() }
function scale(x,y,p){return {x:p.x*x, y:p.y*y}}
function scalePoint(x,y){return function(p){ return scale(x,y,p) }}
function translate(x,y,p){return {x:p.x+x, y:p.y+y}}
function translatePoint(x,y){return function(p){ return translate(x,y,p) }}
function scalePath(x,y,path){ return path.map(scalePoint(x,y)) }
function translatePath(x,y,path){ return path.map(translatePoint(x,y)) }
function clonePath(path){ return path.map(id) }

function wavePoint(p) {
var np = {
x: (p.x + 1.5*Math.cos((p.x+p.y) / 15)),
y: (p.y + 1.5*Math.sin((p.x+p.y) / 15))
}
var d = Math.sqrt(np.x*np.x + np.y*np.y);
var s = Math.pow(1/(d+1),0.7);
return translate(5, 24, scale(s*30, s*30, np));
}
function wavePath(path){ return path.map(wavePoint) }
function drawbg() {
var canvas = document.createElement('canvas');
canvas.width = 1400;
canvas.height = 700;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#333';
ctx.fillRect(0,0,canvas.width,canvas.height);
var g = ctx.createRadialGradient(50,50,0, 50,50,1400);
g.addColorStop(0,'rgba(0,192,255,1)');
g.addColorStop(1.0,'rgba(255,0,255,1)');
ctx.fillStyle = g;
var hex = polygon(6);
var s = Math.sin(Math.PI/3);
var c = Math.cos(Math.PI/3);
for (var i=-2; i<120; i++) {
for (var j=0; j<8; j++) {
if (Math.random()+Math.random() < Math.pow((Math.sqrt(i*i+j*j) / 55),2)) continue;
var x = i*(1.2+c);
var y = j*(2.4*s) + (i%2 ? 1.2*s : 0);
fillPath(ctx, scalePath(10, 7, wavePath(translatePath(x,y,hex))));
}
}
document.body.style.background = '#333 url('+canvas.toDataURL()+') no-repeat top left';
}

2009-11-10

Metatunnel WebGL now works on WebKit as well


Made the Metatunnel demo WebGL port work on WebKit as well, tested to work with the latest Chromium Linux build.

2009-11-07

Done with canvas3d-tests WebGL port

As far as I know, the canvas3d-tests are now all ported to WebGL. However, I'm using a mix of GLSL 1.20 and 1.30 in the tests, which probably isn't a good idea. Porting all shaders to 1.20 or whatever GLSL ES uses as its base would be a good next step.

Hmm, I have an apigen in the git repo that generates GL constant -checking C++ wrappers for OpenGL functions. That could be hacked a bit to make it create better autotests.

The biggest issue with the current tests is that they only cover a small portion of the API, as I focused on writing only evil tests manually. The rest of the API gets banged by the API fuzzer to see if anything crashes. Evil tests then are me trying to cause a segfault or read back information I shouldn't be able to, and for general array indexing validation. I wrote them for things that do array access, read back data from OpenGL, or that seem iffy in some other way.

So. TODO: Port shaders to 1.20, better autotests, file bugs.

2009-11-05

Started porting Canvas3D tests to WebGL

I started porting Canvas3D tests over to WebGL this morning (apparently insomnia helps.) I've been intending to do that since the summer, but never really got started. Better late than never, eh.

I've been testing the Firefox implementation. If there's a way to test WebKit's WebGL on Linux, I'd be most interested in hearing that.

The Firefox implementation has changed somewhat from the last time I worked on it, so I found some new bugs too :) BindFramebuffer is checking its argument against wrong constants, Tex[Sub]Image2D is quite broken for non-image arguments and some functions don't bork on bad data. Plus a fuzzer segfault and an on-exit segfault (didn't trace those yet.)

I still have around half the existing tests to port... Something to do this afternoon.

2009-11-04

Visitor stats for October

About 1250 visits.

Browser market share: 50% Firefox, 15% Explorer, 14% Chrome, 12% Safari and 6% Opera.

OS market share: 56% Windows, 22% Linux, 20% Mac, 1% iPhone.

The smallest screen resolution was 170x180. The largest was 3840x1024. Most people had something between 800x480 and 1920x1200.

Visitors by continent: 44% Europe, 33% North America, 12% Asia, 6% South America, 3% Oceania, 1% Africa.

By country: 28% US, 7% France, 7% Germany, 5% UK, 5% Canada, 4% Brazil, 3% Japan, 3% Finland, 3% Spain, 3% India.

Hmm...

2009-11-02

How I lost 6 kilos in six months

By running 25 km a week and cooking my own meals.

Blog Archive