How to build a 3D City in 256 bytes with Canvas 2D
How many bytes does it take to render a 3D city like in the legendary Extension by Pygmy Projects that won the Amiga demo competition at Assembly 1993.
<body onload=setInterval(F="for(x=w=c.width|=0,Q=Math.cos;x--;)for(i=0;i<32;i+=X%8==X&&Y%8==Y&&F[X|Y<<3]<':'?w:.5)X=14*Q(t)-i*Q(T=t+x/w-.5)+4,Y=14*Q(t+8)-i*Q(T+8)+4,c.getContext('2d').fillRect(x,i*6,1-i/32,3);t+=88",t=9)><canvas id=c>
That's it. No libraries. No dependencies.
How does it work ?
At 234 bytes there isn't much room to do things the right way, store data, change default properties, generate things procedurally, ... well, anything. So how the hell does it work ?
The shades of grey
fillStyle of Canvas is black. The shades of grey are done by drawing axis aligned rectangle less than a pixel wide. The sub-pixel width introduces anti-aliasing which results in a semi transparent black rectangle. Simple.
It turns out that we do have some data: the source code!
Placing the source code of the main loop into a variable allows us to compare the characters of that string and use it a map.
Since the source code is quite short, it was only possible to have an 8 by 8 grid which maps to the first 64 characters of the main loop. Looking at the ASCII codes used in that string, I decided to place a building for each character that comes before the character
: in the ASCII table. In other words any of the following characters
!"#$%&'()*+,-./0123456789 is a building.
for(x=w= --> ...#.... c.width| --> .#.....# =0,Q=Mat --> .##..... h.cos;x- --> .#.....# -;)for(i --> #.#...#. =0;i<32; --> .#..#... i+=X%8== --> .#..##.. X&&Y%8== --> .##.##..
In order to save 4 bytes with the trigonometry code where two
Math.sin are needed, an alias is created for
Math.cos alone. Adding 8 to the angle gives a decent approximation of
Math.sin. The margin of error is 0.15 radians.
At each frame the camera rotates by 88 radians, modulo 2π that is 0.035 radians, which means it takes ~177 frames to go around the city.
Here we use some good old brute force: A fixed step raycaster.
The raycaster "walks" the city plan and stops rendering little shades of grey when it hits a building. The field of view is 1 radian wide. The camera looks at the center of the city and spins around it.
As the rays are walked further from the camera a shade of grey is drawn onto the canvas. For each step further from the camera, we move a few pixels down onto the canvas and use a lighter shade of grey.
We move on to the next ray and column on the canvas when we hit a building or the bottom of the canvas.
The initial version of MINI DISTRICT was neat as a first shot but let's be honest, it looked cheap and not much like a city. With 22 bytes available, it was high time to crank this up to 256 and bring back the city.
<body onload=setInterval(F="for(X=x=h=c.height|=Q=Math.cos;x--;)for(i=j=1;i<h;c.getContext('2d').fillRect(x*2,i,U^X?1.7:2,j-i/h,i+=j))j^1||((U=X,X=15*Q(t)-i/8*Q(T=t+x/h-.5)+4)|(Y=15*Q(t+8)-i/8*Q(T+8)+4))<8&&F[X|Y<<3]<Q&&(j=19-i/8);t+=88",t=9)><canvas id=c>
As you can see the buildings now have floor dividers, and different windows/shading on their North-South and East-West side.
This uses the same technique as the initial version except that we do the tiny subpixel rectangle in both direction:
One for the vertical gradient of the sky line, the other for the shading of the buildings which is based on the integer difference of the X position of the rays cast from the camera.
If the integer difference is not null, it means the ray hit the building while crossing an East-West line.
Optimized city planing
Alrighty the city planing is 18 bytes in the final version but it does a lot more things. It also does them correctly unlike the initial release.
Restricting the buildings to an 8 by 8 grid is now correct and more compact thanks to the simple formula
(X|Y)<8. It works like charm because it check the values unsigned. Therefore negative values become far greater than 8.
Also since we have a better shading technique we can show more buildings without looking messy. That way we can save two bytes by replacing the
":" by any of the strings or objects available since they would be implicitly cast to a string. The candidate were
Q. The last one proved to produce the most interesting city.
What about the initial version ?
Using these new optimizations should bring the initial version around 220-225 bytes. But who wants to see this now ?
As usual for my demoscene productions, MINI DISTRICT is available on Pouet.net where any comments and gestures are appreciated. Hope you like this little production. It was fun optimizing this effect. Twice.
Other recent projects
There are many experiments and projects like MINI DISTRICT to discover other here.
- WE DON'T MAKE MISTAKES AT DEVONE 2019 Giving the closing keynote DEV ONE 2019, held on April 11 in Linz, Austria was absolutely fantastic. The conference was very well organized with one track, 11 talks about "scale" and 600 wonderful delegates. I learnt so much that day, got confirmation for some good practices but also learnt about many opportunities to improve our work. Also it was a pleasure to meet new people and see familiar faces among the organizers and attendees from Script'17.
- MUSIC FOR TINY AIRPORTS AT WEB AUDIO CONFERENCE The Web Audio Conference 2018, held in September 19-21 in Berlin was a great mix of researchers, web developers, artists and performers presenting their projects. I had the chance to provide a deep dive into music for tiny airports, explaining how to generate hours and hours of music in a handful of bytes.
- LRNZ SNGLRT LRNZ SNGLRT is a minimalist and energetic entry for JS1k 2016 showing twisted Lorenz attractors with ambient occlusion, soft shadows, ... a strong beat & clean design.
- THREAD The "10 print" maze generator in 15 bytes of x86 assembler.
- COTTON CANDY First stab at webGL, in 1k between two nappy changes. It's glitchy and tiny but I quite like this puppy. It ranked #3 at DemoJS.
- WOLF1K The idea of this entry for the JS1K contest was to do the impossible: a 1K remake of the famous WOLF5K that rocked the final edition of the5K. It does not feature guns, evil grins and violence for in WOLF1K there is no room for guns or any form of violence.
Don't be shy; get in touch by mail, twitter, github, linkedin or pouet if you have any questions, feedback, speaking, workshop or performance opportunity.