Skip to content

Instantly share code, notes, and snippets.

@tararoys
Forked from EFHIII/README.md
Last active February 19, 2024 19:40
Show Gist options
  • Save tararoys/fdc36c5ba7c47e05a5659a378d7210e3 to your computer and use it in GitHub Desktop.
Save tararoys/fdc36c5ba7c47e05a5659a378d7210e3 to your computer and use it in GitHub Desktop.
Net3arth. What is it and how to use it.

Net3arth is a flame inspired chaos game based fractal rendering program that runs in the browser.

It aims to be fairly versatile, being able to make things like this: ArcSinhSplits

Aswell as things like this: UtahTreepot

And even this (though it's not great at it): Mandelbrot

Net3arth doesn't have much of a UI, instead relying on the user making fractals with code. The code to make fractals is written in a special syntax (sometimes dubbed 3arthLang) which is purpose made. Raw JavaScript can also be included in the code for added flexibility.

An 3arthLang program consists of up to 7 parts:

  • constant variables
  • custom transforms
  • custom JavaScript functions (which can behave like a custom transform)
  • a buffer function
  • the body
  • the camera
  • the shader

The way I think about the Chaos Game is exactly like the lyrics from one of my favorite musicals, 'Come From Away,' where Beverly, the first female commercial airline captain in history, is telling her husband how to explain to her children where she is after her plane got grounded in 9/11.

  1. Tell the kids I'm alright
  2. Take them into the kitchen And show them the map That we used to put pins in.
  3. One pin for each destination that we flew togethef
  4. Tell them I'm fine
  5. put pin here in Gander

Like pioneering aviator Beverly Bass, mapping the locations of a single person on a world map can lead to a very interesting map of every place she's ever flown. The points on her map are dictated by rules- specifically the rules of where Delta Airlines pays her to fly. Over time, the map gets fuller and fuller with more and more pins.

The net3arth chaos game works the same way. Imagine that we've hired pioneering aviator Beverly Bass away from American Airlines, and she's joining Net3arth Airlines on a fractal flight path.

Beverly's home airport is Origins Airport, located at point 0,0. that's where she always starts no matter where in the world we send her.

Beverly, like most people, have particular patterns to how they live and work- rules that shape their lives. There are a lot of them, and a single person can generally only follow one rule at a time. Net3arth Airlines can certainly only send Captain Bass out to fly one plane at a time.

NetEarth Airlines follows a set if rules for where they are going to send Captain Bass. it's not Captain Bass's choice where she flies. Instead, Net3arth flight scheduler randomly picks a rule and says 'Captain Bass, you're going to fly following that rule today.'

In Net3arth, the set if rules Captain Bass will be using yo twll where yo fly her plane are stored in the body.

Captain Bass gets in her plane and flies to the destination that dispatch told her to fly to.
When she gets there, she calls up her husband to tell him where she's at. Her husband gets out a pushpin and puts her location on the map.

in netEarth terms, Captain Bass'a husband is the imahe buffer. he's remembering where sge is. it takes him a littke whike to figure out where she in on the map, using wgat he kniws about maps, map projections, and locations. He's acting like the Net3arth camera. eventyally he figures it out and gets the pin stuck into the correct place on the map.

meanwhile, Net3arth Airlines has sent Captain Bass new instructions to fly to a new location. Net3arth just picked a rule from the body of rules they are allowed to use to send Captain Bass places. So she gets in her plane, follows the new instructions, and ends up someplace new.

The body is the set if rules that tell Captain Bass where to fly.

the camera is the type of map Captain Bass's husband is pushing pins into to map where hus wife is.

Captain Bass's husband notices that his map is pretty complicated, and it's gard to explain to their children what the map neans. So before showing the map to his children, Captain Bass's husband simplifies the map by erasing all the unecessary i formation from it. in Net3arth terms, this eould be called using a shader to render the final map.

At the start of runtime, theres one point at the origin. The properties of the point are sent through the body, then the resulting point is copied, one copy going through the camera and rendered to the image through the buffer while the other copy is sent back through the body recursively.

When the image is rendered, each pixel is first sent through the shader.

to repeat:

the body controls where Captain Bass is foing to fly. the camera controls the size, shape, and othef, fancier things thar Captain Bass's husband is mapping her destinations on. the shader controls exactly how Captain Bass's husband is going to draw all the little pixels on her map. usually they control things like color- is captain Bass's husband using red pen? Black pen? rainbow markers? that's the shader's job.

The body, camera, and shader consists of some combinations of control functions, built-in transforms, and custom transforms.

The control functions are:

  • choose
  • xaos
  • switch
  • sum
  • product
  • sumColor
  • productColor

Here's a simple program that uses each component:

buffer() {
  return averageBuffer();
}

getFraction(number numerator, number denominator) {
  return numerator / denominator;
}

const green = getFraction(1, 20);

transform coloredCircle(object color):
  blurCircle()
  -> color(color);

body:
choose{
  1:
    coloredCircle(colorRGB(0.1, green, 0));
  1:
    coloredCircle(colorRGB(0, green, 0))
    -> circleInv();
};

camera:
scale(1 / 2);

shader:
gamma(2.2);

output

Lets go through it piece by piece.

buffer() {
  return averageBuffer();
}

This is the buffer. It's bassically identical in syntax to a custom function but it has to be named 'buffer'. You can write a custom buffer, but there are a few built-in buffers you can use by just returning them, which is what's done here.

The averageBuffer() built-in buffer makes each pixel the color of the average of all points that have visited that pixel. There's also zBuffer() which only keeps the closest pixel by z-value, firstBuffer() which only takes the first point to visit a pixel, lastBuffer() which only takes the last point to visit a pixel, and the default buffer when none are specified takes the color sum of all points to visit a pixel.

getFraction(number numerator, number denominator) {
  return numerator / denominator;
}

This is a normal custom function. In this case, it's not behaving like a custom transform, it's just a math helper function (and not a very good one). Parameters are typed as any of bool, number, complex, array, object, or function.

const green = getFraction(1, 20);

This is a constant value. This value will be computed at compile time and then inlined wherever it's used. In this case, it's calling our custom function which will return 0.05.

transform coloredCircle(object color):
  blurCircle()
  -> color(color);

This is a custom transform. In this case it takes a color parameter, and colors are of type object. This custom transform applies blurCircle which draws a circle, and then a color() which sets the color of the circle that was drawn.

body:
choose{
  1:
    coloredCircle(colorRGB(0.1, green, 0));
  1:
    coloredCircle(colorRGB(0, green, 0))
    -> circleInv();
};

This is the body. The first thing in the body is a choose. choose is a control function where within it, it will randomly choose one of the listed control paths, run it, and then exit to whatever comes next. Each control path starts with a number which is that path's weight. If, for example, there were two paths

  1: // code;
  2: // code;

the one weighted 1 would be chosen ~33% of the time and the one weighted 2 would be chosen ~67% of the time.

On the fist path, we have coloredCircle(colorRGB(0.1, green, 0));. This calls the custom function defined earlier. As a parameter, it passed colorRGB(0.1, green, 0); colorRGB is a built-in helper function that builds a color object. In this case, the color made is a brownish color.

On the second path, we have:

  coloredCircle(colorRGB(0, green, 0))
  -> circleInv();

The -> operator pipes the result of the left side into what's on the right side. This path is very similar to the first but it draws a dark green circle and pipes it into a built-in function, cirlceInv. circleInv transforms everything within the unit circle to outside it and everything outside the unit circle to inside it. The result here is that all of space is filled dark green except for a circle in the middle.

camera:
scale(1 / 2);

The camera here is pretty boring, it just scales everything by a half so that the circle is fully in-frame. This could just as well be in the body since the body isn't doing any recursion stuff, but that's not always the case.

shader:
gamma(2.2);

The shader is also pretty boring, here just doing some normal gamma correction.

This example was intentionally verbose just to show the different parts of the code. You could simplify the code to being:

buffer() {
  return averageBuffer();
}

body:
blurCircle()
-> choose{
  1:
    color(colorRGB(0.1, 0.05, 0));
  1:
    color(colorRGB(0, 0.05, 0))
    -> circleInv();
};

camera:
scale(1 / 2);

shader:
gamma(2.2);

Now let's draw a fractal.

const cos30 = Math.cos(30 * DEGREE);

body:
choose {
  1: translate(0, -1);
  1: translate(cos30, 0.5);
  1: translate(-cos30, 0.5);
}
-> scale(1 / 2);

camera:
translate(0, cos30 / 4)
-> scale(0.5 / cos30);

shader:
normalizeColors()
-> gamma(2.2);

image

This is pretty standard, this is just a classic example of the Chaos Game. In the body, you have the point randomly moving in one of three directions, and then it's scaled down by 50%. This can also be understood as the point moving halfway to one of three points. The result is the Sierpinski Triangle.

Here, normalizeColors is used in the shader instead of using the averageBuffer. Since not all points are hit equally often, using averageBuffer results in a worse image.

There are many other ways to draw the Sierpinski Triangle; heres one.

body:
translate(0, -1)
-> pointSymmetry(0, 0, 3)
-> scale(1 / 2);

and if you wanted to make the code even shorter, here's one more:

body:
mobius(0.5, -0.5i, 0, 1)
-> pointSymmetry(0, 0, 3);

Moving on to another classic fractal, here's the cantor set construction:

body:
choose{
  2:
    blurSquare()
    -> scale2(1, 0.1);
  1:
    scale2(1 / 3, 0.5)
    -> translate(1 / 3, 0.2)
    -> choose{
      1: identity();
      1: flipX();
    };
};

camera:
translate(0, -1 / 5)
-> scale(1.9);

shader:
normalizeColors()
-> gamma(2.2);

Cantor Set

The first step is simply creating a rectangle representing the 0th iteration:

blurSquare()
-> scale2(1, 0.1);

That's in a choose, so sometimes we'll go back to the 0th iteration, sometimes we'll go down an iteration.

To go down an iteration, we move the previous iteration down and scale it horizontally by a third:

scale2(1 / 3, 0.5)
-> translate(1 / 3, 0.2)

That gets us half of the next iteration, to get the other half, half the time we flip it across the X-axis.

-> choose{
  1: identity();
  1: flipX();
};

For more complex examples, see: https://thecrystalize.github.io/Net3arth/gallery/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment