Before the introduction of generative graphics tools, creating detailed designs was a time-consuming and manually intensive process. This was especially true for designs involving complex patterns. Generative graphics tools introduced automation and algorithmic techniques into the creative process, empowering artists and developers to push their creative boundaries.
In this tutorial, we’ll introduce Alma, a new generative graphic platform, and discuss its features and capabilities. We’ll also demonstrate how easy it is to use Alma’s interactive playground to create generative graphics.
To get the most value from this tutorial, you should have the following:
The concept of generative art and graphics has roots in the early days of computer programming and digital art. Artists and programmers have been experimenting with algorithmic and generative techniques since the mid-20th century.
One of the pioneering figures in generative art is Georg Nees, a German artist who, in the 1960s, used algorithms to create artwork. Subsequently, other artists and researchers, such as Frieder Nake and Vera Molnár, contributed to the development of generative art and graphics.
Generative graphics tools are software applications or programming frameworks that enable visual content creation through algorithms. These algorithms are used to generate digital art, interactive installations, data visualization, and creative coding.
Here’s an analogy that may help explain this concept further: imagine that generative graphic tools are automated, self-playing instruments. Composers create the algorithms that define the rules and patterns. The instruments, guided by the algorithms and instructions, can produce relaxing melodies, harmonies, and rhythms without direct human input for each note.
Developers are like the composers in this analogy, defining the algorithms and instructions. The generative tool interprets these algorithms to create visual compositions, just as the self-playing instruments interpret musical scores to produce music.
Alma is a unique platform that harnesses the power of generative graphics, allowing users to create beautiful visual effects and graphics through a node-based system. Alma is comparable to Blender’s node editor, a popular tool for 3D artists and designers. Similar to Blender, Alma’s approach is centered around nodes, connections, and code nodes, offering a versatile toolset for crafting dynamic visual experiences.
Alma finds utility across various creative domains. Whether you’re an artist, a designer, or a developer, Alma can serve your needs in different ways. Here are some potential use cases for Alma:
N.B., as of this writing, Alma is still in the alpha stage, so there may still be some challenges associated with implementations
Alma’s playground is where our creative journey begins. Let’s take a look to better understand how we can use this tool to create our own generative graphics. The playground comprises several essential components, each serving a distinct role in the generative graphics creation process.
Nodes are the fundamental building blocks within Alma’s circuit. They represent various operations, such as trigonometry logic, texture sampling, color manipulation, and lighting calculations:
These nodes have input and output ports, allowing users to create complex networks of operations:
In the above example, we can see two nodes with their inputs and outputs. First, there’s a VECTOR
4
node that takes in four float inputs (the F
stands for float) and outputs a four-component vector (VEC4
).
We connect the VEC4
output to the VEC4 COLOR
input of the second node, WEBGL CONTEXT
, which is the root node of the graph that injects its color into the renderer.
Connections serve as the pathways for logic and data flow throughout our circuit. They ensure that data moves seamlessly between nodes, enforcing a strict type system to maintain consistency.
Here’s the connection from our previous example:
The GLSL (OpenGL Shading Language) node is an option for those who want to implement complex logic quickly. Users can input a GLSL function, which Alma automatically converts into a node that’s compatible with other nodes in the circuit.
This is a really handy feature. A couple of Code nodes, like CREATION
, are already available from the Alma playground:
In order to see the code, we can click the <> icon in the GLSL function node:
The toolbar provides convenient utilities to enhance user experience. It includes options for viewing the final GLSL code, accessing examples for inspiration, adding new nodes to our circuit, importing and exporting serialized circuits, and enabling a fullscreen mode for an immersive experience:
Alma offers a collection of prebuilt graphic effects in its playground that we can modify and experiment with. Some examples include GRADIENT
, NOISE
, REPETITION
, and CREATION
. These serve as starting points for our creative projects:
Let’s use Alma’s playground to create a simple noise effect graphic. In this step-by-step guide, we’ll introduce the interface, demonstrate how to manipulate nodes and connections, and provide insights for creating visually appealing generative graphics.
First, let’s visit Alma’s playground. There’s a short tutorial there that serves as a basic introduction the playground:
Now, let’s click Next to enter the playground. This opens up an example playground that we can play around with to get a feel for the components and how they work:
For the purposes of this tutorial, we’ll create a new playground interface.
The WebGL Context node is required to render graphics. It’s currently a bit tricky to add this node using the toolbar, so instead we’ll just import a circuit containing the node.
A circuit is basically a JSON file that generates the nodes and connections of a playground. To import a circuit, we click the IMPORT/EXPORT button, represented by a save icon (the fourth icon from the left in the below toolbar:
Once the text field is open, we’ll enter the following code:
{ "id": "946cbf0c-555c-4a23-8871-153033dcb299", "name": "Alma WebGL Starter", "nodes": [ [ "dfb2ecbb-0845-471d-b910-2fdaf575c26b", { "id": "dfb2ecbb-0845-471d-b910-2fdaf575c26b", "name": "WebGL Context", "type": "WEBGL_CONTEXT", "data": { "position": { "x": -388, "y": 263 } }, "inputs": { "color": { "id": "22370f15-7e0d-4529-9444-f6763afff4a5", "name": "Color", "type": "vec4", "defaultValue": { "tag": "lit", "type": "vec4", "val": [ { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 1 } ] }, "value": { "tag": "lit", "type": "vec4", "val": [ { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 } ] } } }, "outputs": {} } ] ], "connections": [] }
Now, we should have something like this in the playground:
To create a static noise effect, we’ll need three additional nodes:
To add the UV node, we click the + button in the toolbar, or right-click anywhere on the circuit canvas, and then select NEW NODE > ACCESSORS > UV:
The UV Node has three Vector 2 (V2
) outputs; we’re interested in the ASPECT CORRECTED
output. This is where the random node comes into play.
Next, let’s click the +NEW NODE, or right-click on the canvas, to create a new node. Then, we’ll select NEW NODE > COMMON > GLSL:
Now, we’ll enter the following code:
float random(vec2 p) { vec2 K1 = vec2( 23.14069263277926, // e^pi (Gelfond's constant) 2.665144142690225 // 2^sqrt(2) (Gelfond–Schneider constant) ); return fract( cos( dot(p,K1) ) * 12345.6789 ); }
At this point, the playground should look like this:
This node takes a Vector 2 (VEC2
) input and outputs a float (F
).
In order to display graphics, we’ll need to provide a Vector 4 input for the WebGL Context node. Let’s click the + button in the toolbar, or right-click on the canvas. Then, we’ll select NEW NODE > VECTOR > VECTOR 4:
Once the node has been created, we can enter the random float (F
) output from the RANDOM
node into the VECTOR 4
node, like so:
This will produce the noise effect:
Here’s the generated code for the Static Noise effect
graphic:
{ "id": "946cbf0c-555c-4a23-8871-153033dcb299", "name": "Static Noise effect", "nodes": [ [ "dfb2ecbb-0845-471d-b910-2fdaf575c26b", { "id": "dfb2ecbb-0845-471d-b910-2fdaf575c26b", "name": "WebGL Context", "type": "WEBGL_CONTEXT", "data": { "position": { "x": -197, "y": 422 } }, "inputs": { "color": { "id": "22370f15-7e0d-4529-9444-f6763afff4a5", "name": "Color", "type": "vec4", "defaultValue": { "tag": "lit", "type": "vec4", "val": [ { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 1 } ] } } }, "outputs": {} } ], [ "d0b1e7a5-c0d9-4665-9a7f-177b4a926bd4", { "id": "d0b1e7a5-c0d9-4665-9a7f-177b4a926bd4", "name": "UV", "type": "UV", "data": { "position": { "x": -1055, "y": 532 } }, "inputs": {}, "outputs": { "aspectCorrected": { "id": "1876fc2e-5876-4739-8d4d-065588e7344e", "name": "Aspect Corrected", "type": "vec2" }, "uv": { "id": "4aa3fb2c-cef1-4cf0-848c-dce738555fea", "name": "UV", "type": "vec2" }, "fragCoord": { "id": "8412d3a3-c97e-403a-b845-287342a274d7", "name": "Frag Coord", "type": "vec4" } } } ], [ "a6b8018e-00b9-4ea0-acbb-f497b21806a9", { "id": "a6b8018e-00b9-4ea0-acbb-f497b21806a9", "name": "Random", "type": "GLSL", "data": { "glsl": "float random(vec2 p) {n vec2 K1 = vec2(n 23.14069263277926, // e^pi (Gelfond's constant)n 2.665144142690225 // 2^sqrt(2) (Gelfond–Schneider constant)n );nn return fract( cos( dot(p,K1) ) * 12345.6789 );n}", "position": { "x": -773, "y": 689 } }, "inputs": { "p": { "id": "c38394cc-3175-4e27-b3f9-f5c5ac24b202", "name": "p", "type": "vec2", "defaultValue": { "tag": "lit", "type": "vec2", "val": [ { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 } ] } } }, "outputs": { "output": { "id": "420b0fa1-3edd-4892-a1b7-dc0dec3467ed", "name": "Output", "type": "float" } } } ], [ "76d22e0f-f9b4-48db-831b-c084bad5129a", { "id": "76d22e0f-f9b4-48db-831b-c084bad5129a", "name": "Vector 4", "type": "VECTOR_4", "data": { "position": { "x": -483, "y": 651 } }, "inputs": { "x": { "id": "ebb5d644-5865-4f47-9334-c67fae2aea61", "name": "X", "type": "float", "defaultValue": { "tag": "lit", "type": "float", "val": 0 } }, "y": { "id": "605b83ac-83d0-4256-b1cf-d3c2ed9e6d6f", "name": "Y", "type": "float", "defaultValue": { "tag": "lit", "type": "float", "val": 0 } }, "z": { "id": "93ccd2d6-fb68-47ff-8e6e-c3591426df54", "name": "Z", "type": "float", "defaultValue": { "tag": "lit", "type": "float", "val": 0 } }, "w": { "id": "8ab84ce4-0373-4dfa-9afb-040debd5f198", "name": "W", "type": "float", "defaultValue": { "tag": "lit", "type": "float", "val": 1 } } }, "outputs": { "vector4": { "id": "0808e28c-f00f-43d7-becb-bc72dc5b03b9", "name": "Vector 4", "type": "vec4" } } } ] ], "connections": [ [ "01078152-6cf7-4c6f-8b28-3ea800462c18", { "id": "01078152-6cf7-4c6f-8b28-3ea800462c18", "from": "1876fc2e-5876-4739-8d4d-065588e7344e", "to": "c38394cc-3175-4e27-b3f9-f5c5ac24b202" } ], [ "5e230d0d-ec71-481b-9c73-3fb90b199ff1", { "id": "5e230d0d-ec71-481b-9c73-3fb90b199ff1", "from": "420b0fa1-3edd-4892-a1b7-dc0dec3467ed", "to": "ebb5d644-5865-4f47-9334-c67fae2aea61" } ], [ "06fbd536-8aad-457c-a77e-75b9e166582c", { "id": "06fbd536-8aad-457c-a77e-75b9e166582c", "from": "420b0fa1-3edd-4892-a1b7-dc0dec3467ed", "to": "605b83ac-83d0-4256-b1cf-d3c2ed9e6d6f" } ], [ "9d4cc697-8807-4478-9f7d-d721d6073401", { "id": "9d4cc697-8807-4478-9f7d-d721d6073401", "from": "420b0fa1-3edd-4892-a1b7-dc0dec3467ed", "to": "93ccd2d6-fb68-47ff-8e6e-c3591426df54" } ], [ "3b1bd33c-bd6c-4d6d-bb2f-c03e19915467", { "id": "3b1bd33c-bd6c-4d6d-bb2f-c03e19915467", "from": "420b0fa1-3edd-4892-a1b7-dc0dec3467ed", "to": "8ab84ce4-0373-4dfa-9afb-040debd5f198" } ], [ "402faf84-44b2-45d7-bcbf-b83cd845168d", { "id": "402faf84-44b2-45d7-bcbf-b83cd845168d", "from": "0808e28c-f00f-43d7-becb-bc72dc5b03b9", "to": "22370f15-7e0d-4529-9444-f6763afff4a5" } ] ] }
Let’s demonstrate how to use Alma’s playground to create a gradient effect. To start, we’ll set up a blank playground by removing existing nodes or by importing a circuit.
To create the gradient effect, we’ll need four nodes:
The WebGL Context node is required to render the graphics. We’ll import the circuit as we did earlier in this tutorial by clicking the IMPORT/EXPORT button, represented by a save icon:
Once the text field is open, we’ll enter the following code:
{ "id": "946cbf0c-555c-4a23-8871-153033dcb299", "name": "Alma WebGL Starter", "nodes": [ [ "dfb2ecbb-0845-471d-b910-2fdaf575c26b", { "id": "dfb2ecbb-0845-471d-b910-2fdaf575c26b", "name": "WebGL Context", "type": "WEBGL_CONTEXT", "data": { "position": { "x": -388, "y": 263 } }, "inputs": { "color": { "id": "22370f15-7e0d-4529-9444-f6763afff4a5", "name": "Color", "type": "vec4", "defaultValue": { "tag": "lit", "type": "vec4", "val": [ { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 1 } ] }, "value": { "tag": "lit", "type": "vec4", "val": [ { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 } ] } } }, "outputs": {} } ] ], "connections": [] }
The playground should look like this:
Next, we’ll add a couple of nodes to create a gradient effect.
To add the UV node, let’s click the + button in the toolbar, or right-click anywhere on the circuit canvas, and select NEW NODE > ACCESSORS > UV:
Next, we’ll add a Swizzle node that destructs a vector and returns its individual components. Then, we’ll click the + button in the toolbar, or right-click anywhere on the circuit canvas, and select NEW NODE > VECTOR > SWIZZLE:
Now, we’ll add a Vector 4 node that takes in four float values and outputs a four-component vector. We’ll click the + button in the toolbar, or right-click anywhere on the circuit canvas, and select NEW NODE > VECTOR > VECTOR 4:
Now, let’s connect our nodes by following these steps:
UV
output of the UV
node to the SWIZZLE
vector inputX
and Y
float output of the SWIZZLE
node to the X
and Y
input of the VECTOR
4
nodeVECTOR
4
output to the COLOR
input of the WEBGL CONTEXT
nodeWe should have something like this:
We can animate the gradient by adding a TIME
node. Let’s click the + button in the toolbar, or right-click anywhere on the circuit canvas, and select NEW NODE > ACCESSORS > TIME:
Next, we’ll add a SINE
node which returns the sine of the given input. Click the + button in the toolbar, or right-click anywhere on the circuit canvas, and select NEW NODE > TRIGONOMETRY > SINE:
Now, we simply connect the TIME
output to the SINE
input and the SINE
output to one of the VECTOR
4
inputs like so:
Here’s the resulting gradient:
Here’s the output code:
{ "id": "946cbf0c-555c-4a23-8871-153033dcb299", "name": "Alma Gradient", "nodes": [ [ "dfb2ecbb-0845-471d-b910-2fdaf575c26b", { "id": "dfb2ecbb-0845-471d-b910-2fdaf575c26b", "name": "WebGL Context", "type": "WEBGL_CONTEXT", "data": { "position": { "x": 345, "y": 243 } }, "inputs": { "color": { "id": "22370f15-7e0d-4529-9444-f6763afff4a5", "name": "Color", "type": "vec4", "defaultValue": { "tag": "lit", "type": "vec4", "val": [ { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 1 } ] } } }, "outputs": {} } ], [ "17394fd6-1018-4b26-877e-31b03e1d97db", { "id": "17394fd6-1018-4b26-877e-31b03e1d97db", "name": "UV", "type": "UV", "data": { "position": { "x": -556.875, "y": 270.375 } }, "inputs": {}, "outputs": { "aspectCorrected": { "id": "8b41dade-d1bc-4673-874b-b24f33351a91", "name": "Aspect Corrected", "type": "vec2" }, "uv": { "id": "d91659e2-7c05-4997-9dbf-e0b9d62e2d20", "name": "UV", "type": "vec2" }, "fragCoord": { "id": "7b6a3eb5-eb34-42ec-9a23-90eb04fca837", "name": "Frag Coord", "type": "vec4" } } } ], [ "41e47893-dc50-441f-b657-c2ee326fbf87", { "id": "41e47893-dc50-441f-b657-c2ee326fbf87", "name": "Swizzle", "type": "SWIZZLE", "data": { "position": { "x": -266.5, "y": 311.75 }, "type": { "selected": "vec2", "options": [ "vec2", "vec3", "vec4" ] } }, "inputs": { "vector": { "id": "367c3cd3-291d-4738-bd75-c18f7033a90b", "name": "Vector", "type": "vec2", "defaultValue": { "tag": "lit", "type": "vec2", "val": [ { "tag": "lit", "type": "float", "val": 0 }, { "tag": "lit", "type": "float", "val": 0 } ] } } }, "outputs": { "x": { "id": "528b5162-993c-48e5-b4e9-7928e66bd40c", "name": "X", "type": "float" }, "y": { "id": "ae87ed86-f192-413b-8789-c9c585b411c9", "name": "Y", "type": "float" } } } ], [ "b84da403-ca3d-4870-a959-4f115c8c1b5e", { "id": "b84da403-ca3d-4870-a959-4f115c8c1b5e", "name": "Vector 4", "type": "VECTOR_4", "data": { "position": { "x": 35.5, "y": 220.75 } }, "inputs": { "x": { "id": "5896372f-e794-46f4-a291-90ceac5a47d9", "name": "X", "type": "float", "defaultValue": { "tag": "lit", "type": "float", "val": 0 } }, "y": { "id": "5b9d42fe-a6c4-4b24-a1aa-b3735517a875", "name": "Y", "type": "float", "defaultValue": { "tag": "lit", "type": "float", "val": 0 } }, "z": { "id": "068e80ed-14b6-4d95-963d-c21ff1e7c752", "name": "Z", "type": "float", "defaultValue": { "tag": "lit", "type": "float", "val": 0 } }, "w": { "id": "53199eef-7046-4d1e-b344-6229b96e86cf", "name": "W", "type": "float", "defaultValue": { "tag": "lit", "type": "float", "val": 1 }, "value": { "tag": "lit", "type": "float", "val": 1 } } }, "outputs": { "vector4": { "id": "587fac1d-8b8f-4fb6-856c-5cb26c2eb9aa", "name": "Vector 4", "type": "vec4" } } } ], [ "be523300-a5bb-4887-b5d4-117b4aac4aa6", { "id": "be523300-a5bb-4887-b5d4-117b4aac4aa6", "name": "Time", "type": "TIME", "data": { "position": { "x": -621.25, "y": 75.375 } }, "inputs": {}, "outputs": { "time": { "id": "912f8e39-973d-408c-96e8-0aa4feaa5d10", "name": "Time", "type": "float" } } } ], [ "da05fa64-73fe-4e8a-b24e-614e5c1319b0", { "id": "da05fa64-73fe-4e8a-b24e-614e5c1319b0", "name": "Sine", "type": "SINE", "data": { "position": { "x": -293.36376953125, "y": 37.45458984375 }, "type": { "selected": "float", "options": [ "float", "vec2", "vec3", "vec4" ] } }, "inputs": { "input": { "id": "55203142-60ef-44f7-b536-1fbd882da361", "name": "Input", "type": "float", "defaultValue": { "tag": "lit", "type": "float", "val": 0 } } }, "outputs": { "output": { "id": "83e99f93-871f-49be-9d64-0e3e9e2a2b51", "name": "Output", "type": "float" } } } ] ], "connections": [ [ "09352645-3ce4-4959-aa8a-ec0f2bb0fbaf", { "id": "09352645-3ce4-4959-aa8a-ec0f2bb0fbaf", "from": "d91659e2-7c05-4997-9dbf-e0b9d62e2d20", "to": "367c3cd3-291d-4738-bd75-c18f7033a90b" } ], [ "23aa3199-e044-420d-849e-956724db0cc5", { "id": "23aa3199-e044-420d-849e-956724db0cc5", "from": "528b5162-993c-48e5-b4e9-7928e66bd40c", "to": "5896372f-e794-46f4-a291-90ceac5a47d9" } ], [ "5f5b361d-2a32-482c-94ee-4b31e2a65ed4", { "id": "5f5b361d-2a32-482c-94ee-4b31e2a65ed4", "from": "ae87ed86-f192-413b-8789-c9c585b411c9", "to": "5b9d42fe-a6c4-4b24-a1aa-b3735517a875" } ], [ "d5593a33-796d-41c5-97ff-0c63b4b4a707", { "id": "d5593a33-796d-41c5-97ff-0c63b4b4a707", "from": "587fac1d-8b8f-4fb6-856c-5cb26c2eb9aa", "to": "22370f15-7e0d-4529-9444-f6763afff4a5" } ], [ "52046d4b-9ab7-448e-9a5f-f2b2c9885d32", { "id": "52046d4b-9ab7-448e-9a5f-f2b2c9885d32", "from": "912f8e39-973d-408c-96e8-0aa4feaa5d10", "to": "55203142-60ef-44f7-b536-1fbd882da361" } ], [ "80579b46-e4f0-42c0-a7ba-103740d90111", { "id": "80579b46-e4f0-42c0-a7ba-103740d90111", "from": "83e99f93-871f-49be-9d64-0e3e9e2a2b51", "to": "068e80ed-14b6-4d95-963d-c21ff1e7c752" } ] ] }
Alma is a creative platform that encourages exploration and experimentation with generative graphics. Its intuitive interface enables artists, designers, and developers to easily bring their visions to life.
This generative graphics tool has an extensive array of nodes, connections, and code nodes, offering endless possibilities for creating dynamic visual experiences. Since Alma is still in alpha stage, we can expect even more extensive and enhanced functionality to come!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.