An homage to Wolfenstein 3D in 251 bytes of HTML5
Looking back at the code of TEA STORM and MINI DISTRICT, I realized that the setup I got there had a lot more potential. Rendering some sort of axis aligned dungeons like the ones from Wolfenstein 3D should be a breeze, and it was.
Since you are here, chances are you will appreciate watching the development version of wolfensteiny which shows the map with the position of the camera and its view cone, along the time to render the frame.
Source code
<body onload=E=c.getContext("2d"),setInterval(F="t+=.2,Q=Math.cos;c.height=300;for(x=h;x--;)for(y=h;y--;E.fillRect(x*4,y*4,b-d?4:D/2,D/2))for(D=0;'.'<F[D*y/h-D/2|0?1:(d=t+D*Q(T=x/h-.5+Q(t)/8)&7)|(3.5+D*Q(T-8))<<3]&&D<8;b=d)D+=.1",t=h=75)><canvas id=c>
That's it: 251 bytes of dungeon horror!
How does it work ?
It is a small small world. With only 251 bytes there is no space to store data, change many properties, generate things procedurally, ... well, anything. You have to think out the box and use some less than subtle techniques. So how on Earth does it work ?
Tiny castle
At such small size, there is no space to store or generate a map, so we are left with using the only data we have: The source code.
Indeed, placing the source code of the main loop into a variable allows us to compare the characters of that string and use it as a map of the empty/solid cell on a grid.
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 is or comes before the character .
in the ASCII table. In other words any of the following characters !"#$%&'()*+,-.
is a wall. Choosing this specific character cleared a straight line in the map which allowed to have a simple camera path.
t+=.2,Q= ----> ░█░█░█░░
Math.cos ----> ░░░░█░░░
;c.heigh ----> ░░█░░░░░ Houston,
t=300;fo ----> ░░░░░░░░ <---- we have a
r(x=h;x- ----> ░█░░░░░█ clear path
-;)for(y ----> █░█░░░█░
=h;y--;E ----> ░░░░██░░
.fillRec ----> █░░░░░░░
Camera path
As mentioned above, the camera moves on a straight line, from left to right, indefinitely and looks alternatively left and right by as much as 0.125 radians as time goes by.
Dirty trigonometry
In order to save 4 bytes with the trigonometry code where two Math.cos
and 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.
The rendering
Here we use some good old brute force: A fixed step raymarcher.
Unlike Wolfenstein 3D which is really processed in 2D and needs a single pass per vertical line on the screen, Wolfesteiny operates fully in 3D. This is a curse and a blessing because it's far less efficient, but on the other hand it is far more compact and trivial to implement.
Rays are cast from the origin of the camera in the view frustrum, and for each pixel of the rendering surface, we walk slowly along each ray to check whether we are between the floor and ceiling and haven't hit a solid cell in the map. The further the rays is walked, the bigger and darker the shade of grey for that pixel.
for(x=h;x--;)for(y=h;y--;/* draw a shade of grey D in x,y */)for(D=0;'.'<F[/* position of the ray in the map */]&&D<8;b=d)D+=.1
The outmost loops go through each x,y pixel and draw the resulting shade of grey. The inner loop walks along the ray corresponding to the x,y pixel until it hits a solid cell in the map.
The floor and ceiling collisions i checked using the integer coordinate of the Y component of the ray. When it is falsy, zero, the ray is between the floor and ceiling. Otherwise the ray reached the floor or ceiling and we can 'jump' to the position of a solid cell in the map:
D*y/h-D/2|0?1:/* ... */
The other components are computed and composed like this to check the map:
(d=t+D*Q(T=x/h-.5+Q(t)/8)&7)|(3.5+D*Q(T-8))<<3
Which really means X | Y << 3
, but let's break this down:
// the angle the camera looks at is
cA = Q(t) / 8
// the angle the current ray is
rA = x / h - .5 + cA
// D represents how far we have walked
// along the ray which gives us
X = t + D * Q(rA)
Y = 3.5 + D * Q(rA - 8)
The resolution
The default resolution of a Canvas is 300x150. Setting either the width or height clears and resets the Canvas so to maximize the resolution at the lowest cost, it is best to simply set the height to 300 at each frame.
Thus our canvas is 300x300 but we can not walk 9,000 rays for each frame. By dividing this by 4 we end up casting, and walking, 75x75 = 5,625 rays. Since we walk in 0.1 increments until we reach a solid cell or reached a distance of 8, this account for up to 75x75x80 = 450,000 tests per frame.
The shades of grey
The default fillStyle
of Canvas is black. There is no space to change it so we need to find a way to get more colors. The shades of grey are done by drawing axis aligned rectangle with a fractional width and height. The sub-pixel dimensions introduce anti-aliasing which results in a semi transparent black edges. Simple. On top of that the animation and the dithering improves the illusion of several shades of grey.
Shading
Like in MINI DISTRICT, we check the integer X coordinate of the ray before and after hitting a wall to know whether we hit a wall facing Noth-South or East-West. You remember the main loop ?
t+=.2,Q=Math.cos;c.height=300;for(x=h;x--;)for(y=h;y--;E.fillRect(x*4,y*4,b-d?4:D/2,D/2))for(D=0;'.'<F[D*y/h-D/2|0?1:(d=t+D*Q(T=x/h-.5+Q(t)/8)&7)|(3.5+D*Q(T-8))<<3]&&D<8;b=d)D+=.1
The important parts here are the b-d?4:
, d=
and b=d
which store and compare the consecutive integer X coordinate of the ray.
Why the variable names b
and d
? Because it looks like a cute owl when you compare them: b-d
. That's why.
Missing anything ?
If any piece of the puzzle appears to be missing, I invite you to read the posts about TEA STORM and MINI DISTRICT which are the foundation of WOLFENSTEINY and cover additional dirty details.
Feedback
As usual for my demoscene productions, WOLFENSTEINY is available on Pouet.net where any comments and thumbs up are appreciated. Hope you like this little homage.
Other recent experiments
There are many experiments and projects like WOLFENSTEINY to discover other here.
- FRONTFEST MOSCOW It was an honour to be invited to Fronfest Moscow 2017 with the little family to give my first workshop; implementing a Twin-stick shooter using ES6 and Canvas, and to continue my CODE🎙ART series of talks + live coding aiming to inspire new web developer artists. on November 18th, 2017
- VOLTRA VOLTRA: Grinding the Universe, a gritty JavaScript demo, winner of the 1024 bytes demo competition at the Assembly 2017. on August 6th, 2017
- BREATHING EARTH Another take on Nadieh Bremer mesmerizing Breathing Earth visualisation, running at 60fps on a 2D Canvas without libraries or frameworks. on June 26th, 2017
- MINICRAFT Tribute to MINECRAFT, voxel flyby in 252 bytes of HTML5 on November 2nd, 2013
- JAVASCRIPT IS JARIG Javascript is 18 years old! Let's celebrate with a nice little tune. on September 14th, 2013
- 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. on July 2nd, 2011
- MANDELBROT TRACER Possibly the smallest Mandelbrot tracer ever in JavaScript: 101 bytes on September 21st, 2008
- STARFIELD A simple 3D starfield with title and fog in 256 bytes of Javascript on February 25th, 2007
Let's talk
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.