FSM. Notes on the work or: An attempt to explain everything and nothing
written by julian_hespenhe...
TABLE OF CONTENTS
- Introduction
- What is (a) FSM?
- Cybernetics
- World-building: Flatland and Terraforms
- The Grid + Cardinal directions
- The FSM: Spending time with it and watching sequences unfold
- On randomness() and $fx.rand()
- Finally what are the drawing agents
- Base agents
- Spatio agents
- Temporal agents
- Special agent
- fx.iterations(), Fisher-yates shuffle, MB32
- Color Palettes, KERNELS
- Characters, Glyphs, Chardinals
- Making things look sharp
- URL Features, Embed tool, Controls
- Progressive Web Application
- Controls and sharable
- Exhibition in Berlin
Since 2021 I have been experimenting with textmode only. Years before that I worked more on a pixel or fragment basis. Personally, for me, the whole departure into the world of AI is nothing I want to mess with. We have so much technology already we can rely on and we are not even using most of it to its fullest potential. I started to specifically search for the application of "low tech" in everyday use and connected it to my prior knowledge and experiences as a practicing designer and creative technologist.
So for a year, I have conducted everyday experiments to further develop my own taste, branch out into different directions, and retract again if something didn't feel right. Later that year in 2022 it was about time that I felt like I could create a small work or rather "algorithmic edition" which I originally wanted to call "STATEMACHINES". The plan for STATEMACHINES was a release on fx(hash) but I never got around to doing it. Due to several reasons and a friendly approach by Bright Moments to join the Artist-in-Residence program I quickly took parts of the code of STATEMACHINES and turned them into my first long-term generative art project called "KERNELS". A lot of the ideas in KERNELS worked already quite well but I felt like I was too quickly approaching the limits of what the CPU can do with a huge amount of objects on screen. A core idea that I already implemented in KERNELS was the one about emergence: Every single object on the screen follows a very simple ruleset and together they form a bigger visual representation of the underlying core. More about that later.
To overcome this (CPU) limitation, during the spring of 2023 I started to pick a topic I tried to avoid for years, maybe even more than a decade: Shader programming.
It was very obvious that shaders were the missing key and to unlock more potential in my works I needed to get my hands dirty and so I did: For weeks smoke emitted from the top of my head as I tried to change the way I was thinking about programming things but one day it clicked and I could emulate (some) of the mechanics I have built prior. I am saying "some", since there is still lots of wriggle room and I have loads of ideas that I couldn't realize yet, but which I will hopefully do soon.
'FSM' is my first shader-based work and I have built a very small pipeline: A mechanics shader that uses a feedback loop (cybernetics *wink* *wink*) to do some parallel programming to emulate some of the previously object-orientated programming paradigms in a faster way. Then there is a second shader, which is more of a sprite-based approach to visually express the piece on the screen. Actually, there are some more shaders but they are not here for the heavy lifting.
The beauty of all of this is, that everything, I mean all calculations, happens on a super small scale inside of one shader, piping into the next shader and then scaling things up to screen size with a little bit of help of some nifty techniques. More on that, at the very bottom of this text!
So in the end, by harnessing the power of the GPU, things get very speedy which I am quite happy about.
What I like the most about FSM is that in a way, it's not only a work where I put 100% of my brain work into — and may have in turn made things a little bit too overcomplex - but it's also a celebration of things I have done for the last few years and what some people may know me for. For me, it's more than bringing two worlds together: Things I like with the things that I want. Things that I know and things that I desire in and for the future. Things that made me happy and things that are making me happy.
It is both a very derivative work and yet so pleasingly transformative in new ways.
What is (a) FSM?
If you have collected FSM you may or not have heard of the term finite-state machine. For those who know, it is quite clear what it means and how it should function, but I would also like to welcome everyone else who is still uninitiated: A finite-state machine is a model for designing behaviors, actions, and transitions in automata theory. We can see and experience simple FSMs all around in everyday life. Let's take a classic example: A traffic light. It controls the flow of lanes in a street by using three different states:
Red, yellow and green light.
To go from a red light (Traffic is signaled to stop) to a green light (Traffic can go) it has to transition to and through a yellow light (Attention: State change). This means we have a finite amount of states and the programming of the traffic light is also expressing the behavior and duration of the different states. Outputs from the system we perceive classically depend on the current state and the next state depends on the current state's internal processing of input. It becomes apparent that an FSM has some sort of memory, which carries over from state to state: The output of one state ideally becomes the input for the next state.
If we go back to the traffic light, there is also clarity in its design: The order in how the lights are positioned has to be "fast readable" or "quickly accessible" by all participants of traffic: There can be no confusion on the order of how the lights change (Either upwards or downwards direction, at least in Germany or Europe). Everyone needs to be able to tell fast what is the current state of their own lane.
If you think about it for a second, we can recognize that a simple FSM like a traffic light can control the flow of cars, bikes and pedestrians. How is that so? We can zoom out from a micro-level FSM to a macro-level FSM: A whole crossing. Let's take a four-way crossing, with four different lanes of cars. On this zoom level, always two lanes are supposed to be clear for "flow" and two need to be stopped.
The traffic flow program can take things such as interrupts from pedestrian lights to bring a lane prematurely to a stop. This means that FSMs can also have interruptions in their cycles and sequences. This is somewhat of an emergent behavior since it supersedes the normal programmed flow by injecting the human into a sequence again.
Now that we know the very basics of state machines and their finite abilities to switch around, getting interrupted but also continuing their sequence, we'll move on to the next section.
Emergence. Cybernetics and Autopoiesis
Previously I've mentioned emergent behavior as a design pattern to get a human subject into a sequence. That's partially misleading and I only threw it in, to introduce the term to you. Originally and in short the emergence of an emergent behavior or property happens in a complex entity, that the original simple part didn't have on its own.
Let's grab a simple from nature: Flocking behavior in so-called Murmurations.
You may have seen in real life or in some videos huge flocks of starlings flying around in a unified way, drifting like tidal waves through the sky. This is a very beautiful prime example of emergence in action.
Every single starling can only do a few things: Flap the wings (Control speed), go up, down, left or right (Control direction), and use its eyes to take care of not crashing into other Starlings (Separation and cohesion).
So a single bird can freely roam the sky, but if you put a couple hundred of them together all of sudden they also need to interact with each other. The actions of one Starling have impacts on its direct neighbors.
Let's say a few Starlings get pushed around by changes in the wind and this has a major impact on the whole flock: Ripple effects that cause a whole directional change, since every single Starling still wants to stay close to its neighbors.
There is no single leader in the flock that dictates the direction, only dynamic complex individual decisions that ripple through the system.https://www.youtube.com/watch?v=fmtD5pkxG04
Now we've learned a bit about emergence. It's quite a beautiful yet complex topic and just like pre-programmed FSMs, these dynamic systems are also all around us: Snowflakes have complex fractal patterns, ripple patterns in sand dunes emerge only from the interaction between wind and the surface properties, crowds of humans can either express "crowd intelligence" to a single question or go into "mass panic", where only a few subjects know what the actual cause of panic is.
Emergence is, if you want to, also a core principle of Cybernetics.
At the beginning of the 20th century, scientists and mathematicians started to develop a new theory of controlling nature through the use of circular causal feedback. A well-known definition of cybernetics comes from Norbert Wiener, who has characterized it as "control and communication in the animal and the machine".
Cybernetics
First-order cybernetics, the foundational stage of cybernetic theory, emerged in the mid-20th century and focused on the study of systems and control. It sought to analyze and regulate systems from an external observer's perspective, emphasizing the control and feedback mechanisms within those systems. First-order cybernetics aimed to create models for understanding and predicting the behavior of complex systems. Central to this approach was the idea of maintaining stability and homeostasis within systems by adjusting external variables based on feedback. An illustrative example of first-order cybernetics can be found in the regulation of a thermostat, where the system's response is based on input signals and desired set points.
Second-order cybernetics, a subsequent development, introduced a paradigm shift by recognizing the role of the observer in the system. This perspective acknowledged that the act of observation itself influences the behavior of the system being studied. Unlike first-order cybernetics, which treats observers as external entities, second-order cybernetics integrates the observer into the system, acknowledging the inseparable relationship between the observer and the observed. This approach brings reflexivity and subjectivity into cybernetic discourse, emphasizing the co-evolution of the observer and the observed. Humberto Maturana and Francisco Varela, prominent neuroscientists, played a significant role in advancing second-order cybernetics with their concept of "autopoiesis."
"Autopoiesis," a core principle of cybernetics, refers to the self-organizing and self-maintaining nature of living systems. The term was coined by Humberto Maturana and Francisco Varela to describe how living organisms continuously produce and repair themselves, maintaining their structural and functional integrity. Autopoietic systems are characterized by their ability to self-organize and adapt to changes in their environment while remaining distinct and coherent entities. Maturana and Varela's work not only deepened our understanding of the fundamental processes of life but also had profound implications for philosophy, cognition, and the study of complex systems. Their groundbreaking ideas laid the foundation for second-order cybernetics, highlighting the dynamic interplay between the observer and the observed.
Furthermore, the KERNELS / FSM color palettes "Maturana" and "Varela" are especially named after Humberto Maturana and Francisco Varela. These palettes draw inspiration from their contributions to cybernetics and reflect the nuanced interplay of colors and shades, just as their theories emphasize the interplay between observer and observed in complex systems. The palettes serve as a visual representation of the vibrant and interconnected nature of cybernetics and its profound influence on various fields of knowledge.
World-building: Flatland and Terraforms
"Flatland" by Edwin A. Abbott is a satirical novella that explores the social hierarchy and limitations of a two-dimensional world inhabited by geometric shapes, revealing broader commentary on societal norms and perspectives; In the context of a finite-state machine (FSM), Rain in Flatland serves as an analogy for state transitions, while an FSM as an NFT synchronizes screens using a "Rain Function" to update hourly.
In "Flatland," Rain's movement across the two-dimensional plane represents state changes similar to those in a finite-state machine (FSM). Just as Rain transitions from one location to another, an FSM progresses through different states based on inputs and conditions. This analogy helps conceptualize how the FSM synchronizes screens using a "Rain Function" at each hour, updating the NFT's display to reflect the latest state, much like Rain's movement reflects the changing state of Flatland.
FSM is part of my research endeavor in world-building, inspired by the multi-dimensional work "Terraforms" by Mathcastles. In comparison to FSM, Terraforms generates three-dimensional hyperstructures with hidden complexities, akin to the holes in its structure. Similarly, FSM faced challenges with "random probability holes" due to its smaller collection of 100 pieces. To address this, a new approach utilizing fx.iterations was employed to ensure a more coherent and balanced distribution of elements within the collection, creating a seamless and engaging experience for collectors and participants in this world-building exploration.
The Grid + Cardinal directions
As previously mentioned, each FSM mint lives on a grid. Partly borrowed and inspired by Flatland and Terraforms but also Dwarf Fortress, FSM deals with cardinal directions that have an influence on the visuals. A mint can be in one of eight directions, starting with the cardinal directions: North, East, South and West and then also going with the intercardinal directions: North-East, South-East, South-West, North-West.
Some features such as the glyph combinations or the appearance of drawing agents depend on the position within the grid. More on this later.
Another interesting thing would be, that in the very north and very south in the Cardinals North and South the character set gets flipped in its lower parts:
Usually, the first character is reserved for the Spacing Symbol ( , , ·, |, ., -, ♰, º, ヽ, :, +, , ) and the following one for a true blank glyph. In the true North or South where this logic is flipped, FSM mints also have a bit of a different aesthetic feel since the underlying "grid" that is being provided by the spacing symbol may just disappear thus giving the outputs more of a floaty feel.
The FSM: Spending time with it and watching sequences unfold
Before we go into some of the details, here is a word of advice: I suggest you spend your time with your FSM.
Why?
It's a generative artwork that develops over time, slowly revealing its different mechanics to you while you are watching.
The exact reason for this is a sequencing mechanism, that picks a random amount of drawing agents and cycles through them.
The logic behind this looks a bit like this:
- Pick how many agents will be in the sequence (between 1 to 4 agents)
- From a "pool" of available agents in the current FSM pick a few agents to go into the sequence
- Pick a number after how many finished agent runs it should just shuffle the order of the sequence
- Pick a number after how many sequence runs it should build a new sequence
While the sequences are being displayed one by one to the viewer, have a counter that keeps track of when a "blank state" has fired the last time. If after a certain time of drawing agents this "blank counter" has reached a certain limit, fire an emergency "sweep" or "forced blank".
Emergency sweeps or (🧹 in the JavaScript console) are the perfect place to hide some glyph modifications: shift the character set, flip the current state of the fake PETSCII-ness, or flip the current state of whether the whole character set is mirrored.
In a way, emergency sweeps are what make viewing an FSM over a long time very pleasing: The glyphs you see on screen seem to shift a bit around and you may find some new ones entering the visuals while some other glyphs are getting phased out.
On randomness() and $fx.rand()
At one point there have been some questions about randomness generated via the Math.random() vs. fx(hash)'s own random function:
In total FSM is calling the $fx.rand() a total of 227 times during its live runtime. This means every consecutive call of $fx.rand() will push the pseudo and predetermined number chain a bit further. Why does it feel like the outputs vary still?
This question I cannot completely answer. Maybe it's an overwhelming use of random functions in all of the different drawing agents and from there, there is some interplay happening between them which is somehow affecting the randomness.
I don't believe that the 12 targeted uses of Math.random() have an effect on all of the other $fx.rand() calls.
Finally what are the drawing agents
To draw things to the screen there is more than a handful of different so-called drawing agents. They are programmed to follow a pre-programmed animation pattern, that can vary to certain degrees because of the usage of $fx.rand() applied to the parameters.
The agents can be divided into three groups:
- Base agents
- Spatio agents (Spatially distributed)
- Temporal agents (Distributed or activated by time)
Base agents
Base agents are available for most if not all collectors of FSM. Here is a short overview of their names, their IDs and what they do specifically:
Blanker - ID #0
Is only able to erase the screen. It has 13 ways to do this so, e.g. erasing the screen from bottom to top or from left to right, in circular movements, and so on. Every time a Blanker, actually any of the agents, finishes its drawing operation it will do a reset of all of its internal parameters. When Blanker resets its parameters, it also tries to determine whether it should try to reset the screen with the spacing symbol or with a true blank character.
Spiraler - ID #1
As the name already suggests, this agent draws spiraling patterns to the screen. It has 14 different modes to this so. It's simple and quite straightforward in its movement but sometimes so very satisfying to watch.
Walkers - ID #2
The concept behind walkers is the "random walker", where a certain amount of "dots" are randomly, almost drunken, walk across the screen. There are some simple yet nice modifications available in this agent. One of them is just mirroring the movements on one of the axes or both. This almost creates patterns that are close to a Rohrschach test or look like artificial bugs. There is a work by pixelfiller called neurobugs, which came immediately to my mind. Another slight modification is the surpression of diagonal movements and fixing the agents to either horizontal or vertical movements giving the outputs more of a grid-esque and rigid quality.
project name project name project name
Scanner - ID #3
Is probably one of the most seen agents, since it takes its dear time to either go row by row or column by column over and through the screen. Further modified behaviors this agent can show is the mirroring of its movement and also the use of the *10 PRINT CHR$(205.5+RND(1));* algorithm: A very simple algorithm, that generates a random number per cell with a 50/50 outcome and finally picks between one of two glyphs that can be shown. Actually, most if not all agents do have the "10 PRINT" as a variation, too. It makes things visually way more varied and interesting.
Modulo - ID #4
This is the faster version of Scanner. Instead of "stepping" one by one through lines and rows, this agent increases by specified amounts. Unless Scanner, here we cannot find a distinction in running through rows and columns since the movement is converted from a two-dimensional coordinate into a uni-dimensional coordinate. Increasing the step lengths draws patterns to the screen that can go at various angles through the grid.
LineOrder - ID #5
Splitting the canvas vertically into two parts, this agent will travel with different means from top to bottom along the splitting line: Rotating, expanding, and/or shrinking.
Spatio agents
Continuing after the base agents we have the spatial agents. These are drawing agents, that are spatially distributed by the FSM.
Geo - ID #6
Patterns that are coming from Geo are in my opinion very nice and tidy: Squares, Triangles, Circles and Lines. There is a whole bunch of composition modules from which the FSM will pick. 126 to be precise.
Interestingly enough: Geo has a two-tiered distribution.
For Geo to appear in the FSM, your cell needs to be in the right place. There is a three-day rotation of CIRCLE, SQUARE and TRIANGLE, so chances are quite high that at least one day Geo will turn up in your FSM. The three-day rotation can be considered the second tier for distribution and a proto-temporal distribution.
The formula to calculate when Geo appears is quite easy: Day of the month % 3 = x
This simple modulus operation restricts the input Day of the month to just three positions. You can also try on your macOS by starting your spotlight and instead of using the % just type the word mod
eg.
- a July 1 2023 is the first day of the month: 1 % 3 = 1
- July 2: 2 % 3 = 2
- July 3: 3 % 3 = 0
- July 4: 4 % 3 = 1
So in a circular fashion, each day of the month will result in a number from 0 to 2.
The SQUARE configuration is for days with the result of 0
CIRCLE configuration for days with result 1 and
TRIANGLE configration for days with result 2.
Wind - ID #20
If your cell's y-coordinate is smaller or equal to 2, then your cell finds itself in the north where strong winds may be seeded within your sequences. These winds are combinations of sin() and cos() functions harmonizing in a playful dance.
Streak - ID #21
On the other hand, if your cell's y-coordinate is bigger or equal to 7, you'll find that your FSM is a "southern" one. The visual difference to the north is that, instead of having very wavy and organic lines you'll encounter more of a strict environment. A grid-esque landscape, that is being inspired by the rigid nature of rice fields and brown coal mining sites.
Various configurations here run along the lines of how the grid has been divided.
Noise - ID #22
This agent only appears around the borders of the grid. It is distributed in a castellated fashion and is here to remind us, that the landscape of FSM is a finite one. Its concept is heavily derived from "render distance" limitations of old computer graphics, where certain objects at far distances couldn't be displayed anymore and to save on memory or performance, old computer programs instead started to render a thick "fog" to obscure the view.
Temporal agents
Rain
This agent will appear and re-appear every day at a certain time. When the clock strikes the full hour, for a minute and only a minute at max and when the hour is not the 20th of its iteration, the rain from the north will fall down.
This re-occurring event will drizzle light to heavy raindrops coming far from the north and momentarily synchronize all FSM cells — showing that they all inhabit the same grid.
Nightfall
This agent occurs once per day. Similar to the rain, it will synchronize all of the FSM pieces, but instead of showing its effects in a very disruptive way like the rain it creeps in over a longer period (15 minutes). Only at the end of this period are its effects also the strongest, so maybe for a long time you may not notice that the night has fallen.
Warp
Warp aka "The Wizard" is a mysterious event that happens only once a day at a certain time in a certain cell. It could be your cell, it could also be a neighbor's cell. No one knows really.
Special agent
Mirror
Double-click your FSM
fx.iterations(), Fisher-yates shuffle, MB32
This next part may be a bit more technical. Excuse me while I nerd out for a bit.
I previously mentioned Terraforms by Mathcastles as a huge inspiration and reference for this project, next to Dwarf Fortress and Flatland. One problem I could quickly see from far was that to create a generative piece that works with coordinates but doesn't have "holes" in it because of a symmetrical probability distribution in statistics, or simply called "bell-shaped curve" distribution of data, I needed a way to circumvent this behavior.
First of all: How would it look, if there were no countermeasures to distribute everything evenly?
In three dimensions and if we took the Hypercastle from Terraforms as a model: Very, very sad. If we measure a 10x10x10 cube as a grid, it would become very spotty, maybe even unrecognizable as a landscape or shape in general.
In two dimensions, if flattened like Flatland: Probably a bit better, but then also chances are higher that two or more collectors would get the same coordinates while other coordinates are left completely without any collectors.
I cannot say that I understand statistics at all, but I am assuming to fix bot problems in 2D or 3D, we just need to plot more data points (or have bigger editions) to fill up the probabilistic holes.
The shape that happens in the Hypercastle is, if you look at it sideways, reminds me of a "bell-shaped curve". Maybe shifted a bit towards the upper end.
For the 2D shape, we could just simply increase the points to randomly hit those "holes" with just more mints, but then again I am very unknown to most of you in this space and the chances of me selling out a 300 to 900 edition of FSM was more than unlikely.
That's where fx.iterations() comes into play: I asked the fx(hash) team if they had plans to implement a feature, similar to ArtBlocks Mint Nr. function and I got a positive reply. Well, if they hadn't had it I would've not realized FSM anyway.
In the end, I needed a way to pre-determine and "hand out" cell numbers/coordinates to collectors effectively and automatically at the time of minting.
The naive way would be to say:
- Mint Nr. 1: Coordinate X: 0, Y: 0
- Mint Nr. 2: Coordinate X: 1, Y: 0
- Mint Nr. 3: Coordinate X: 2, Y: 0
- …
- Mint Nr. 99: Coordinate X: 8, Y: 9
- Mint Nr. 100: Coordinate X: 9, Y: 9
but that is very boring and in a way straight-up dangerous since it is way too deterministic. The problem here: What if someone wants to snipe for exactly the middle of the grid? Coordinates X/Y: 5/5? They could wait a bit during the auction and when things are rolling trying to get a middle piece. Well, at least trying. At the time of writing the code, I could not tell how much participation and anticipation for this project was out there.
So I opted for a different model:
Build a random-number generator that is entirely deterministic due to a fixed seed, that can easily scramble numbers around and give me for an iteration (the mint nr.) the exact coordinates. By numbers I mean all of the mint numbers in a randomized fashion.
eg.
- Mint Nr. 1 could be cell 55
- Mint Nr. 2 could be cell 12
- Mint Nr. 3 could be cell 76
and so on.
The problem is here, to build a random number generator that is not only deterministic whenever I call it with the same seed phrase but also works in a crossover fashion on any device, system, OS, or browser. There were a few candidates that didn't work, but after a long look and search I found a good combination:
Fisher-yates shuffle is an algorithm for shuffling a finite sequence. I could pass in 100 elements, my cells, and shuffle them nicely with the help of a seed. This implementation is really and by far, the simplest solution and is almost a chaotic/controlled version of the "Towers of Hanoi" algorithm.
Then the seed is hashed by the cyrb128() hash function, which will turn a "spoken word" key into a 128-bit hash value.
The last piece to the puzzle is Mulberry32, a minimalistic generator utilizing a 32-bit state, originally intended for embedded applications. It's quite nice and the JavaScript implementation is blazingly fast. The state is only 32-bit, so the period after the random sequence repeats is less than that for example of a 128-bit state generator. But let's say: 4 billion states is still a good number to pick from.
With these three algorithms in place, fx.iteration() could be safely called to generate the coordinates and other features. At the same time, I can say with high confidence that no holes may appear inside of the grid and that in the end no coordinates are booked double.
Color Palettes, KERNELS
If you have known me for a while, you know that I used to work strongly in monochromatic black-and-white color palettes in the past. Working with limited colors is similar to putting all of your other focus towards other fields: Motion and composition.
At one point I started to play around with colors but with my first long-term generative project called KERNELS, I started to fix a color palette, that was based on the 16 colors of the Commodore 64. Why? Well, I've been using a font that was already very much inspired by the C64 font and had also features like inverting background/foreground colors. So going with the colors was just a natural decision.
The original KERNELS palette collection consists of 64 different individual palettes:
The first 40 palettes (indices #0 until #39) are combinations and recombinations of the original 16 colors, that when paired with the font bring a certain nostalgia feeling to the screen - but at higher framerates and in motion, that has not existed back then.
The last 24 palettes (starting from index #40) are modern and "poppy" combinations. They are just fun.
Names of the palettes are wild, many of the names are references to either a feeling, something I've seen, or whatever I was reminded of when I looked at the palette the first time, some are even straight-up names of cyberneticians. The last color palette in the bunch is one of my favorite ones since it's the color palette I've been using in my code IDEs over the last few years.
In KERNELS collectors had the chance to get a random color palette out of all these options and then on top of the very rare chance also to hit an inverter flag, which would invert the palette for a mint, to a slightly different alt version of the palette.
So in theory, there were 64 color palettes but with invert 128 options.
For FSM I reduced the initial distribution of the color palettes: Each of the cardinal and intercardinal directions was assigned a small pool of palette choices. So having a cell in the North gives different probabilities and choices than a cell in the South or East or West.
Each cell has a base color palette, from the initial 40 KERNELS palettes. Divided by eight directions (North, East, South, West, North-East, South-East, South-West, North-West) I ended up picking five color palettes per cardinal direction.
The distribution looks a bit like this:
- Green plains in the East
- Rocky south
- Sandy west
- Harsh north
The intercardinal act as buffer zones in-between, e.g. The northeast has harsh palettes but also a bit of a "green plains" touch.
For the secondary distribution of color palettes, the remaining 24 palettes, I divided once again by the eight directions and ended up with three secondary color palettes per pool. This secondary palette I will call the "emerging" palette.
But why have two color palettes per cell?
The base palette is the one a collector will immediately see. It is the "true" color of that cell, but shortly, maybe after a cycle or two of the FSM sequence, a fade will happen. The fade is a transition "into" the secondary, emerging color palette.
In truth: The FSM is also a gradient clock, interpolating from 00:00h until 23:59h between these two color palettes.
As an example: At 00:00h (or 12 AM) you'll see the base palette with 100% opacity and 0% of the emerging palette.
Skip forward 12 hours to noon (12:00h / 12PM), and you'll have a perfect 50%/50% mix between the base and the emerging palette.
If we skip another 6 hours forward to 18:00h / 6PM, the base palette is at 25% and the emerging palette at 75%.
You can see that throughout the day the ratio shifts constantly. If you know your FSM, you may at one point also be able to read the time from it.
All of this "mixing" can be stopped by using a URL parameter, so that only the base palette stays forever.
Characters, Glyphs, Chardinals
The characters you can see on the screen are a direct reference to the C64 font. It's a juiced-up, more fun version of the original font with a few more glyphs than what we would find in Code Page 437. While creating the font I have also removed the "inverse" glyphs that were present in the original C64 font. I only can guess why the original font had the inverse glyphs baked in, but in the case of FSM everything is generated on the fly since mostly computational things are cheap and we can hold things in the storage during runtime, and it also gives me more flexibility in what I can do with the characters on screen. It also takes a little bit less space to save those baked-in versions into the font file. If I am not wrong, it's about 50% of the original font size. It's not much but in the end, it is data, that doesn't have to be loaded from IPFS and sent to you.
The glyps in general are kept very blocky, with a few exceptions, so that at low resolutions of the FSM grid things look nice and chunky with that little "oomph!".
The next one is an important and interesting aspect of FSM: The distribution of the character sets. I have talked before about north / south swaps of spacing symbols and the blank glyph and how emergency sweeps can switch or rather modify what you see in the FSM. The character set distribution is in comparison to the color palette distribution a little bit more straight-forward: The overall grid of FSM is 10x10, so we have the coordinates from 0 to 9 on the x-axis and 0 to 9 on the y-axis respectively.
The cell's coordinates are in the end a combination (string concatenation) of two pools of character set coordinates:
Character sets for the x-axis:
- 0 : "⌠⌡"
- 1 : "↑↗→↘↓˙"
- 2 : "×÷♯♰"
- 3 : "█▋▌▍▎▏▕"
- 4 : "♥♦♣♠"
- 5 : "#$%&'()*+,-./"
- 6 : "0123456789"
- 7 : "◤◤◢..◤◤◢◢"
- 8 : "╭╭░░░░░╮╮"
- 9 : "いキチヘベペミモ"
Character sets for the y-axis:
- 0 : ".,·:;⋰`º"
- 1 : "⋰⋱/:_◜◞◠+*`=?!¬•"
- 2 : "+−×÷=≠><≥≤±≈~¬"
- 3 : "╷┬┐┌─╴╶╵┴┘└│┤├"
- 4 : "▁▂▃▄▅▆▇█░▒▓█"
- 5 : "█▂▂▂▂▂▂▂█"
- 6 : "▇▚▚▚▞▞▇█▌▐▄▀░▒▓▓"
- 7 : "█▓░░▒▒▒▒▓"
- 8 : "░░▒▓▓▒▒▒░"
- 9 : "░░█▄▒▓▀░▄"
Let's build some examples:
- Cell 0/0 has the sets ⌠⌡ and .,·:;⋰`º
- Cell 1/0: ↑↗→↘↓˙ and .,·:;⋰`º
- Cell 3/9: █▋▌▍▎▏▕ and ░░█▄▒▓▀░▄
You can see how the coordinates are mapped one-to-one to the character sets. This is in general also a very even distribution, since we are in a 10x10 grid, there are always 10 collectors who eg. have the character set x at 0 -> ⌠⌡
To spice this a little up and not make every 10th FSM cell look the same, we have "Chardinals" - a portmanteau of "characters" + "cardinals". You may have guessed it already: Chardinals are character sets that also take the cell's cardinal direction into consideration.
For each of the 8 cardinal directions there exists a pool with four additional character sets that will be added to the x + y character set concatenation.
The pools look like this:
North
- ▇▚▚▚▞▞▞▞▇
- —|
- KLMNOPQR
-
East
- ▅▂▅▃▂▃▃▂▅
-
- 1234567890¹²³¼½¾√∑
- →→→
South
- ▂█▂▂▂▂██▂
- ╳/\\//╳////\\
- ⎾▞⏌
- ◤◥◢
West
- █▄░░▒▓▀░▄
- ☼
- ╭╯╲╱├
- ←←←
North-east:
- ▝▒▛▒▝▅░░▒
- ◥◢┼◣◤
- STUVWXYZ
- ☹
South-east
- ▌▄█░▒▓▓▀▐
- ╲╱
- ♭♮♪♫♬
-
South-west
- █▌▐▄▀░▒▓▓
- ╲╱◥◤
- ╔╗╝╚
-
North-west
- ▆▇▆▇▉▊▋█▊
- «{<(;:~
- ABCDEFGHIJ
-
If we put the spacing symbol and blank glyph at the front, a whole character set of cell looks a little bit like this:
True north / true south: [blank glyph] + [spacing symbol] + [character set x] + [character set y] + [chardinal set]
Everywhere else: [spacing symbol] + [blank glyph] + [[character set x] + [character set y] + [chardinal set]
Just for a reminder, these are the spacing symbols: " ", , ·, |, ., -, ♰, º, ヽ, :, +, ,
And the spacing symbol is randomized by the fx.hash() function
Some examples
- Cell 0/0 (NW) : · ⌠⌡.,·:;⋰`º«{<(;:~
- Cell 6/9 (E) : + 0123456789░░█▄▒▓▀░▄1234567890¹²³¼½¾√∑
- Cell 9/9 (SE) : ヽ いキチヘベペミモ░░█▄▒▓▀░▄♭♮♪♫♬
These character sets are generated first and then two more factors are added to further lengthen the chain:
- Fake PETSCII inversion? (on or off)
- Mirroring set? (on or off)
Let's start first with inversion: It's the classic inversion technique that can be seen with the original C64 font. In the case of FSM, everything except the spacing symbol and blank glyph will be copied to the end, and the background and foreground will switch places to invert the character set.
This introduces more contrast to the output.
The second option is mirroring. Once again, everything except the spacing/blank symbol are copied, but this time the colors stay the same and the order will be mirrored.
eg. 0123456789░░█▄▒▓▀ turns into ▀▓▒▄█░░9876543210
Now you see, this can easily increase the character set to lengths that can become very uncomfortable to handle. With shaders we have 255 bits of information per channel, so theoretically we could also store sets with 255 characters. Practically and when everything is in motion it turns into a fast blur and becomes very unattractive to watch. So I've played around with some values and found that somewhere around 16 to 24 in character set length is quite enjoyable. So the last key to having our final character set is an offset and a wrap-around technique:
Let's just take Cell 0/0 again and I'll use code tags because then I can use monospace to show you how many characters will be "selected".
Also: xx are the positions of spacing and blank.
+ 0123456789░░█▄▒▓▀░▄1234567890¹²³¼½¾√∑
xx
It has a length of 39 characters, but we only want to see 24 of these at a time:
+ 0123456789░░█▄▒▓▀░▄1234567890¹²³¼½¾√∑
xx[----------------------]
The offset is at position 0 and shows characters from position/index 2 until 25 (because of spacing/blank glyphs): + 0123456789░░█▄▒▓▀░▄12345
If we offset this by 1, we'll get this: + 123456789░░█▄▒▓▀░▄123456
+ 0123456789░░█▄▒▓▀░▄1234567890¹²³¼½¾√∑
xx-[----------------------]
Offsetting a bit more (offset to 10): + ░░█▄▒▓▀░▄1234567890¹²³¼½
+ 0123456789░░█▄▒▓▀░▄1234567890¹²³¼½¾√∑
xx----------[----------------------]
In case our offset reaches the end, then this happens, where the string gets a wrap-around: ▀░▄1234567890¹²³¼½¾√∑012
+ 0123456789░░█▄▒▓▀░▄1234567890¹²³¼½¾√∑
xx---]------------[-------------------
It's not really complicated, but I love playing with these little details which make the viewing experience so much more enjoyable.
Making things look sharp
So, when we think about shader programming we think about "glossy" and "glassy", "wobbly" and "blurry" things. I mean, at least I think of that, since 99% of the works I encounter mostly look like that
In comparison to FSM, my previous work KERNELS was completely computed on the CPU and it was quickly shown in certain grid configurations, that the framerate may take a knee dive for the browser.
In FSM I have been developing my own, shader-based textmode / ASCII modules that can do things very quickly and I absolutely love how things don't look like your standard shader. I am not going into details of how the shaders are built, I just want to mention that the whole grid, let's say a 128x128 grid is happening on a canvas that is 128x128 pixels big. On the monitor/screen where you are reading this right now, this is just a small square probably a little bit bigger than the surface your thumb would take on the screen.
KERNELS already had a similar approach, but a few problems with scaling: Things got under certain circumstances a little bit blurry.
Obviously, I did not want to repeat this mistake so right after KERNELS was released I wanted to take the energy and fresh insights and look into options on how to make things look sharp on screens, regardless of the initial canvas size and the end size (4k? 8k?) and preferably without the help of shaders first.
This turned into some experimentation with chunky pixels and an attempt of trying to find some solutions. Luckily @p1xelfool and @itsgalo joined the conversation back then on Twitter.
We all had some different findings, but the key elements here are:
1. (Galo and me) Resizing the canvas via CSS by using canvas.style.imageRendering = "pixelated";
The function I wrote for FSM looks like this and should be called in setup() or on windowResized()
function crisp() {
canvas.style.width = windowWidth+"px";
canvas.style.height = windowHeight+"px";
canvas.style.imageRendering = "pixelated"; // crisp-edges no good on chrome :/
}
2. (p1xel via REAS) In order to use shaders we need to use WEBGL in p5.js to draw on the GPU. This brought a whole new load of problems that could be solved like this:
let tex = [];
const canvas = document.getElementById("defaultCanvas0");
const ctx = canvas.getContext("webgl");
ctx.imageSmoothingEnabled = false;
tex[0] = mainCanvas.getTexture(interpreter.display());
tex[0].setInterpolation(NEAREST, NEAREST);
What does this do?
You are telling the program to grab the main canvas (where everything is ultimately going to be drawn to) and switch the "smoothing mode" from "weird blurry smoothing" to "nearest neighbor scaling". What is nearest neighbor scaling? Well, in short, it involves resizing an image by duplicating the existing pixels and placing them in a grid pattern to make the image larger or smaller, which can result in a blocky appearance.
By grabbing the texture and setting the interpolation, we are making sure that small pixels don't blur out when we scale them up and stay nice and chunky 👌
3. Setting pixelDensity(1) on all canvases (main drawing canvas and all off-screen buffers) is obviously also a step that needs to be done.
4. Modifications for p5.js 1.0.0
The thing is, before we (Bright Moments and I) decided to release 'FSM' on fx(hash) we were originally thinking about having this project on ETH / ArtBlocks. One of the hard limitations there was that a project could only use one library and in the case of p5.js it would be limited to version 1.0.0.
It is a nice release version, but lacks one defining feature: Setting the interpolation.
Oh boy, did things look like crap! I was smashing my head against this problem for a good week or so. How do you fix the graphics, if the library itself can't do it and you can't update since the blockchain version is just fixed?
Here are my findings: The function for setting interpolation is there but gets completely ignored, due to some oversight. That was just perfect. A perfect mess.
So I needed to go deep into the library and overwrite it somehow. Here are my findings and I hope they hope if you ever need to release a chunky-pixelated-p5js-artblocks-project :)
I hope one day they can bump the version so this huge block of code (and also expensive bytes) is a thing of the past:
p5.Texture.prototype.init = function(data) {
var gl = this._renderer.GL;
this.glTex = gl.createTexture();
this.glWrapS = this._renderer.textureWrapX;
this.glWrapT = this._renderer.textureWrapY;
this.setWrapMode(this.glWrapS, this.glWrapT);
this.bindTexture();
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); // <-- nearest!
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); // yay
if (
this.width === 0 ||
this.height === 0 ||
(this.isSrcMediaElement && !this.src.loadedmetadata)
) {
// assign a 1x1 empty texture initially, because data is not yet ready,
// so that no errors occur in gl console!
var tmpdata = new Uint8Array([1, 1, 1, 1]);
gl.texImage2D(
this.glTarget,
0,
gl.RGBA,
1,
1,
0,
this.glFormat,
gl.UNSIGNED_BYTE,
tmpdata
);
} else {
// data is ready: just push the texture!
gl.texImage2D(
this.glTarget,
0,
this.glFormat,
this.glFormat,
gl.UNSIGNED_BYTE,
data
);
}
};
In the end, I kept this code still in the FSM release on fx(hash) since that little bit of code is not that expensive to have there in case things go wrong.
URL Features, Embed tool, Controls
Finally, let's talk about modifications via URL parameters.
There are a few, that can visually modify FSM. These parameters also come in handy in case you want to display an FSM in a gallery/exhibition setting and always need to start it with custom parameters.
First I'll just list what these parameters do and then I give you some examples. I'll have a tool to modify these settings a little bit easier soon.
Grid: The standard grid is 64 (for the long side) and the shorter side is proportionally scaled to it. This value can be set as low as 2 and as high as you want. Setting the grid doesn't work for mobiles, which is a little oversight on my side and I want to apologize for this!
Border: The border around the FSM. This parameter can take negative and positive integers. Negative borders are something of "it's not a bug it's a feature" since it sort of zooms into the FSM. One border unit equals the space of one glyph. The standard is 2 (practically two characters space between the browser window and the actual grid)
Scale: This parameter is important if you want to start exporting frames / GIFs from the FSM. The standard scale 1. So a 64x64 grid is 64x64 pixels * fontSize (8*8 pixels) resulting in a frame that is 512*512 pixels + borders.
Framerate: The standard is 120 fps (target), but this can be set to anything lower or higher, too. The minimum: 1 fps | max.: ?
GIF length: The standard length for a GIF in seconds (when you normally hit the [g] key). The standard is 3 seconds, but you can go as high as you want. Be careful with what your browser is capable of doing still!
Automix: Standard is on. This means, FSM will interpolate between the base and the emerging color palette with the help of the progress of your IRL day using the system clock. Turn this off and you'll be left with the base colors.
Let's take an example: FSM #21 currently owned by Mitchell (https://www.fxhash.xyz/u/miiVault) and modify it with several parameters before we put them together into one cohesive modified FSM.
project name project name project name
Let's add a smaller grid by putting the grid parameter at the end of the URL. We'll make this a little bit more "coarse": &grid=16
Border: &border=10
Scale to 6x. Test it by grabbing a frame with [s]. It'll be a big PNG: &scale=6
GIF length of 6 seconds via. Press [g] to capture the GIF at the new fixed length: &gif=6
Automix off to force base palette: &automix=false
Framerate set to 12 FPS: &fps=12
So if we want to combine these parameters, we can simply do something like this:
&grid=16&border=10&scale=6&gif=6&automix=false&fps=12
An upside of smaller grids is that the sequences are a bit more speedy. Now that you know this: What's your favorite configuration?
Progressive Web Application
Before I come to the last point, I just wanted to note that FSM was built as a progressive web application. It is supposed to run on any device that is capable of running WebGL graphics and that includes smartphones and tablets.
So you can try adding FSM to your Home screen and have it always at hand. On iOS this gives you a "App"-like feeling without the Safari browser interface.
Controls and sharable
Here are the controls. If you'd like to display the FSM in full screen, press [f] on the keyboard. This will also automatically remove the mouse cursor, so you don't have to awkwardly try to hide it on the side of the display.
- [f] To enter fullscreen mode
- [spacebar] Play or pause the animation
- [s] Export current frame as a PNG
- [g] Capture a small GIF
- [G] Press once to start capturing a GIF and press [G] again to stop the capture process.
More unofficial: You can press on your keyboard the "Arrow key down" key to open the in-app debug menu. It will show you data like the framerate, gridSize, current character set and active drawing agent.
The capturing of GIFs is also meant to make FSM more shareable. I hope you can share your beautiful moments with me!
Exhibition in Berlin
Thanks to Bright Moments I had the wonderful opportunity to exhibit FSM in the heart of Berlin's gallery street. One day after the initial auction of FSM we presented the sold pieces in the beautiful rooms of the Bright Moments Berlin gallery on 8 screens. A textile banner had some texts were explaining a little bit more about the work, but it wasn't as in-depth as these writings here. It was a wonderful show (and full house) and was accompanied by the sweet sounds by local DJ CHENG NWSH.
Thanks to everyone who came that night. My heart is still full!
And thank you to the Bright Moments team, especially Malte and Ameesia, and fx(hash) crew, Paul, Olha, cosimo, for all of your support. Thank you for giving me the space, the platform and your efforts in promoting this project.
Photographs 📸 Helge Mundt
Video by TonyGaSch from Bright Moments.