10 votes

3D Sierpinski Pyramid in 140 Characters of Javascript

2 comments

  1. [2]
    KilledByAPixel
    Link
    I didn't write the code for this but I made the GIF and I think this may be the best GIF I've ever made, wanted to share it with you all. Just a super smooth 60 fps loop with cool subject and tiny...

    I didn't write the code for this but I made the GIF and I think this may be the best GIF I've ever made, wanted to share it with you all. Just a super smooth 60 fps loop with cool subject and tiny file size.

    Live javascript code here...
    https://www.dwitter.net/d/13680

    2 votes
    1. [2]
      Comment deleted by author
      Link Parent
      1. unknown user
        Link Parent
        There's a superficial explanation of the variables down the page: u(t) is called 60 times per second. t: elapsed time in seconds. c: A 1920x1080 canvas. x: A 2D context for that canvas. S:...
        • Exemplary

        There's a superficial explanation of the variables down the page:

        u(t) is called 60 times per second.
        t: elapsed time in seconds.
        c: A 1920x1080 canvas.
        x: A 2D context for that canvas.
        S: Math.sin
        C: Math.cos
        T: Math.tan
        R: Generates rgba-strings, ex.: R(255, 255, 255, 0.5)
        

        So, it's already not 140 characters, given that it does handwave-y substitution outside of the legal code snippet. It's like saying your whole website's JS engine is 4kB heavy, even though for it to work you have to load two different 10kB-ish libraries.

        But – fair enough, the code structure is fun in itself.

        Here's a slightly more readable version of the code:

        function u(t) {
        	c.width |= f = ( X, Y, Z, d, o = d < 7 && 5 ) => {
        		for (
        			x.fillRect( 960 + 1e3 * X / Z, 1e3 * Y / Z, s = 6 / Z / Z, s );
        			o--;
        			f( X + R * S( a = t + 11 * o ), Y + R, Z + R * C( a ), d + 1 )
        		) R = o && 2 ** -d
        	}
        	f( 0, 0, 1, 2 )
        }
        

        The main-function declaration part – function u(t) {} – is irrelevant, so I'll leave it out.

        It starts off fun, though! The value of c.width (where c, remember, is the JS representation of the HTML <canvas> element) is immediately assigned to the bitwise OR of itself of the immediately-declated function f(). If you know how bitwise operations work – good; if you don't – welcome to the club. I've never met real-life bitwise operations outside of the source code that made the Quake III Arena perform so damn well on 3D rendering on the year-1999 hardware.

        Effectively, c.width is being assigned as either itself or f(), based on the bit (base 2) value of each. How does this work? Again, I dunno. Maybe someone with actual CS experience could help out here.

        f() is declared as an arrow function (f = (argument) => { declarations } is shorter than f = function (argument) { declarations }) with five arguments: the first three for positions in each dimension, d, and o, which is set to default to 5 if d is less than 7. d < 7 && 5 is an inlined conditional and means "if d is less than 7, declare 5". But what is d?

        I'm still not sure. It seems to be an arbitrary value, although why is it not inlined into the function is beyond me.

        Right. Onto the function's contents.

        The only thing f() does is run a for loop, which takes three parameters...

        1. initialization: the declaration of all the variables and other data that are going to be used in the loop

        2. condition: that which permits the loop to run; if condition is false, the loop is stopped

        3. final expression: the expression to be evaluated (run) at the end of each loop

        ...and evaluates the given statement (set after the loop declaration) if the loop's condition is true.

        The initialization is x.fillRect(x, y, width, height) – that is, the command to fill, within the context of the <canvas>, a rectangular area starting at coordinates x and y and of width width and height height with the pre-set fillStyle. In other words: make a certain area of the place you're permitted to paint in the given color, or a pattern, or a gradient – whatever you'd set it to be. There's no mention of fillStyle in the code, so it defaults to black – a legal, and most often default, HTML/CSS color.

        The arguments of the function are comma-separated, which makes golf code like this sometimes difficult to read. In the loop initialization, most arguments are given in complex form: x is set to 960 - 1000 * X / Z (1e3 is the engineering way of saying "1 with three 0's after it"), y is 1000 * Y / Z, and width and height are set to the same value by the virtue of just-in-time variable s declaration, which is immediately reused. The computations are nothing like straightforward, but the code is, so far.

        The condition of the loop is rather clever. o is a number declared previously, and saying o-- – or, <any kind of number>--, for that matter – is saying "use the value of o, and then take 1 away from it". If you have o equal to 5, o-- would mean that 5 is going to be used as a value, and then – after it's been evaluated and after it's relevant to the progression of the function – it's going to be reduced by 1, to make 4.

        It's important because of how Javascript performs soft evaluation. In JS, all values can be coerced to mean either true or false. It's not as straightforward as you might think. For this case, what matters is that any number that isn't 0 is true, in the boolean sense, and only 0 is false.

        When o-- is evaluated to a number that isn't 0, the loop continues to run. With each run, however, the value is decreased, meaning the loop is effectively running its own countdown without having to declare a separate variable for it. Each run makes o less and less, until it's evaluated to 0, which is when the loop stops, because 0 is equivalent to false, and if the condition of the loop is false, the loop ceases to run.

        It's a fairly-common practice among experienced JS programmers to do these things, but to a novice, it's an awe-inspiring revelation. I sure was awed when I first discovered that such a thing is possible.

        The final expression is where the magic happens.

        Long story short: the context of <canvas> is two-dimensional. This means that <canvas> doesn't have the native capacity to show 3D objects. To make 3D painting work, one has to get creative.

        Now, I'm not entirely sure about the nature of the function call, but if I understand it correctly, what the final expression does is paint the 2D space of <canvas> in such a way so as to present it as 3D. Loosely speaking, the loop recursively calls its caller function – f() – in such a way as to simulate three-dimensional movement via the Math.sin (the JS mathematical sine function) and Math.cos (the JS cosine function) and the t variable, which represents the time passed and gives the sine/cosine meaning.

        Effectively, it's a wave function of a spin of a 3D object that isn't there. That's quite clever.

        The statement part of the for loop – R = o && 2 ** -d uses the same inline conditional to set R (see above) to 2 to the power of negative d if o is true (that is, not 0). How R works is beyond me, but I have a feeling that it helps paint the 2D space to represent a 3D image, by only coloring the things that correspond with the faux-3D-object's movement.

        And then, after c.width is set, the declared f() function is called with a set of basic parameters: two zeroes for "start at coordinates X = 0, Y = 0" (which are the beginning of the painting field – the context – of <canvas>), 1 for the Z parameter that gets used within the function, and 2 for the value of d.

        I don't entirely comprehend how the code makes it all work, but for the structure of it, this is the basic overview.

        5 votes