Giter VIP home page Giter VIP logo

arborator's Introduction

Background

This project emerged from my fascination with the subject of a book I stumbled upon: 'The Algorithmic Beauty of Plants.' The author, Aristid Lindenmayer, a botanist, developed a mathematical theory to describe plant topology, which later evolved into a framework for modeling the development of branching structures in general. The Lindenmayer Systems or L-Systems, as they became known, found applications far beyond the field of biology in diverse fields such as urbanism, computer graphics, and even programming languages.

I was inspired to attempt a simple and straightforward implementation in JavaScript.

Before I knew it, what started as curiosity evolved into something of a passion that led to two exploratory projects.

This project, Arborator and FASS (here) are being developed not only to expand my knowledge in procedural generation but also as personal challenge to try and take my coding skills to a new level.

L-Systems Moodboard

L-Systems 101

L-Systems are a specialized subset of formal grammars. They are based on a set of symbols, alphabet, that undergo transformations through recursive and iterative processes. Governed by a specific set of production rules, these symbols are systematically replaced in each iteration, resulting in progressively complex strings.

In essence, an L-System consists of:

  1. An Alphabet: A designated set of symbols.
  2. Production Rules: The symbol and its respective sequence of symbols that will be added to the sequence.
  3. An Axiom: The string used to initiate the iterative processes.

Let's consider the simplest model that can be generated using an L-System: the growth of algae.

For this, we need an alphabet with at least 2 symbols, designated as A and B.

Each symbol represents a stage in the organism's growth:

A signifies a directional segment of growth, such as a filament, stem, trunk, or branch.

B represents a branching node.

Next, we define production rules as follows:

A → AB

B → A

These rules instruct us to replace A with AB and B with A.

The axiom serves as our starting point, initially consisting of a single A.

We initiate the process by applying these production rules to every symbol. The first 5 iterations would appear as follows:

  1. A
  2. AB
  3. ABA
  4. ABAAB
  5. ABAABABA

Continuing this iterative process leads to the expansion of the string. With each iteration, new branches (represented by "B") emerge from existing ones, resulting in a natural, self-replicating pattern.

However, 'ABAABABA' remains an abstraction that provides limited insight into the actual configuration and shape of the growing algae. It serves as a topological representation, illustrating how elements are organized in terms of their relative positions but missing spatial relationships such as distance, angle, and length of growth.

While we understand that 'B' represents branching, it only indicates where branching occurs and what follows. We can discern that two 'A's follow, but we remain unaware of their degree of divergence, their directional orientation, or the extent of growth they represent.

Objectives

This framework has in mind the practical realities of the creative process for whom mathematics and nature are not the goal in itself but rather the tools. It aims to offer a versatile generation engine and set of modular tool adaptable to various creative and design purposes.

As such the whole rationale behind it prioritizes considerations such as how, when, where, and to what extent the user can decide how much of the generative process relies on the algorithm, as opposed to how much is left to his deliberation.

In summary, this framework aims to offer the capabilities of nature-like procedural generation while remaining pliable to the creative vision and intent of the user, whatever that vision may be.

Approach

The sequences produced by an L-System, in isolation, offer limited information about the visual attributes of the structures they model. As seen in the previous example, we know that 'A' represents a growth segment, and 'B' indicates a branching point. And that’s all. We are left wondering: What are the specifics of the growth, such as its length and direction? Is the unit of growth constant? How does the structure branch out? How do the branches diverge, and at what angle?

ABAABABA

So what do we make of this? Where do we go from here?

Algorithmic Beauty of Plants addresses covers this problem, and this framework does not depart significantly from what is proposed there. As with all previous graphical interpretations outlined there it also takes a post-processing approach to the problem as well as adopting many of the conventions outlined in the book.

These are the 3 main features/capabilities:

Syntactic Awareness

The introduction of new symbols into the alphabet seems unavoidable. We need symbols such as ‘+’ or ‘-’ for commands that instruct the drawing tool to turn left or right by a defined number of degrees. Other symbols, while less apparent but equally fundamental, include ‘[’ and ‘]’. These brackets encapsulate instructions for saving and restoring the drawing state, enabling the creation of branching structures.

The resulting sequence might now resemble something like this:

A[B][+A][-A[B]][+A[B][-A]][+A[B][-A][+A[B]]]

As you can see, there's a lot more to be parsed here.

The new sequence mixes different types of symbols and some degree of discernment logic needs to be implemented for reading the sequence in order to retrieve just what is relevant at this stage for the rewriting and replacement of symbols.

The symbol interpretation logic also needs to be abstracted from the symbol manipulation logic for the sake of efficiency and achieving a clear overall architecture.

Drawing a parallel with language, the system doesn't always have to 'read' the symbol. Most of the times, to construct syntactically correct sentences, it simply needs to know that 'this' is a verb and 'that' is a noun. In our specific case, that ‘this’ a drawing command and that ‘that’ is a production rule.

This syntactic awareness is embedded in the system through an ubiquitous concept/object I've called Glyph that is introduced further ahead.

Information Flow

Up to this point, we've considered non-parametric L-Systems ie. for each Production Rule and drawing command there’s a single, fixed outcome. Regardless of how intricate the generated sequences, we are ‘stamping’ the same shapes. We are confined to geometric and mechanical structures.

We’re missing and important property from the system: organicity.

That’s what why parameters are needed. When symbols carry additional information, such as numeric values, conditions, or instructions, we not only introduce a higher level of control but also make the system more intuitive to understand and use. Parameters act as influencing factors, akin to sunlight or nutrients, determining the structure and shapes.

The main problem we face is that the separation between the writing and drawing stages doesn’t allow for Productions to communicate directly. Theoretically, in a L-System each production is processed in parallel and simultaneously, which, in practice, means that a Production won’t have access to the whole and definitive context it needs until the complete sequence is fully rewritten in each iteration step. Furthermore, by the time each rewriting is complete, the connection between a parent Production and its offspring is lost.

This leads us to a critical question: How can we ensure that a descendant production receives and accurately interprets the parameters passed by its progenitor?

Creative Focus

Production rules are the fundamental building block for creation in a system like this. Designing with L-Systems involves understanding and mastering their inherent logic. So, how can we provide an experience that allows individuals to freely explore, experiment, and ultimately master not only the system itself but also the rules and commands they define?

The goal here is to identify what is relevant for the creative stage of coding and how to best abstract everything else away so that a whole creation could be defined in a single place.

Architecture Overview

Sequence writing process diagram

The solutions to the 3 problems/concerns outlined before also corresponds to 3 components:

The Glyphs

Glyphs offer an alternative to a lookup table, which would require frequent access and consultation across multiple modules, creating more dependencies than it would be sensible.

As lightweight wrapper objects, they carry their own type information and bind specific characters to production rules, commands, or instructions, for quick and adequate symbol identification.

By allowing only a single instance for each symbol and binding each symbol to a single function, they safeguard the system against overwriting and functional overlaps.

As objects there’s little to be said about them. They are plain and simple. Their primary utility lies in the syntactic classifications they embody.

Which, at the current time consists of:

Type Description Examples
Rule Represents a production rule Y B
Instruction Represents a drawing command or rendering operation f
Marker Serve as flow controllers and scope annotators during parsing, e.g., delimiter signs ( )

We’ll see each more in detail throughout the documentation where each type is the most relevant.

Glyphs also act as exchange tokens between components. They are the system’s currency in the sense that one component can exchange with another to obtain required data or objects. For example, a component needing a Production Rule object may pass a stream of Glyphs to an interfacing component and receive corresponding Production Rules in return, all without having to decode each Glyph to explicitly request the Production Rules. In this manner, Glyphs facilitate the seamless flow of symbols throughout the system.

The Prims

Think of them as specialized "message bottles" in the long stream of symbols left from one production to another. Prims are like telegraphic pieces of metadata containing information about the type and intended purpose of a parameter. They answer an existential question: "What are you, and what should I do with you?"

Similarly as it is done with Glyphs, Prims carry this information avoiding once more the need for a cumbersome, ever-growing lookup table. Each Prim comes with a prefix that signifies its role, making them easily distinguishable and actionable.

However, there's more to Prims.

They depend on a pair of Marker Glyphs represented by the parentheses characters. Anything enclosed within these markers is treated as a candidate for parameter data. These parentheses serve a dual purpose:

  1. They act as enclosures, isolating the Prims from the rest of the sequence. This makes it simple to identify, extract, or even skip over parameter data when scanning the sequence.
  2. They delegate the task of parsing and interpreting Prims to the very components that need them—Productions and Sprites. By doing this, the system avoids a monolithic parsing mechanism, allowing each component to decode the Prims according to its logic and requirements.

Introducing a specialized subset of symbols such as these allows for better flow control, making the system not just hierarchical but also modular. It allows, for example, for the framework to be applied in both the contexts of parametric and non-parametric L-Systems alike.

The Sprites

Sprites function almost like middleware in a web framework. The Sequencer-Production operates as a request and response mechanism, and it's here that Sprites play an important role.

Sprites are modular components designed to execute specific logic on Productions. They encapsulate a range of generic behaviors for manipulating Glyph sequences and processing Prims. These modules enable users to incorporate multiple functionalities into a single Production by mixing and matching.

Instead of creating a multitude of specialized Production classes, Sprites allow for one unified Production class to be highly configurable by allowing them to be chained or composed in various orders through which the sequence runs like a stream.

To Be Continued...

While this framework already includes a few other components and is generating the first results, it is still in its early stages of development. Many design and architectural decisions have yet to endure serious scrutiny and testing — most of which I still have to learn how to do. However, the three components I've chosen to document here are foundational to the entire idea and unlikely to change much, at least not in terms of their conceptual definitions. I would dare to say.

arborator's People

Contributors

mothervolcano avatar

Watchers

 avatar

arborator's Issues

Improve parameter parsing in Composer

The parsing and packing of parameters should be done more upstream (see comment line 149 in Composer.ts).

Too convoluted:

  1. We get the parameters in a wholesome string from the Production Rules
  2. Split each character of it into the thread array
  3. Splice them out at the end from the final sequence, block by block
  4. Join each block into a wholesome string again.
  5. Split them one last time, this time properly at each ',' separator character.

We should:

  1. Get the parameters in a wholesome string from the Production Rules
  2. Push everything that is in between ( ) as whole string into the thread array.
  3. Split them only at the end when we need them in their own array.

Implement Comprehensive Testing

Description

Create a suite of automated tests to ensure the library functions as intended.

Steps

  1. Write unit tests for individual components.
  2. Develop integration tests for various use-cases.
  3. Run performance benchmarks.

Implement Operator Sprite for Performing Calculations Between Compatible Prims

The objective is to introduce a new Sprite class called Operator. This Sprite is designed to perform arithmetic operations between the values of two compatible Prims that exist within the same Production. Currently, these could be ParameterPrims and CounterPrims, but the implementation should be extensible to accommodate future types of Prims that deal with numeric data.

Behavior:

  1. Perform the specified arithmetic operation between any two compatible Prims (e.g., ParameterPrim and CounterPrim).

  2. Overwrite one of the participating Prims with the resulting value, as per the specification when the Operator is instantiated. The implementation should, however, remain flexible to allow for alternative ways to utilize or store the resulting value in the future.

Glyphs added by Replicator are not parametrizable.

The Replicator sprite is simply duplicating references from the Production Rule glyph directory. It's merely creating echos.

It needs to create new MetaGlyphs even if just within the Production process cycle in order to enable each to be written with different prim values.

Inconsistent String Input Handling Across Different Types of Prims"

Currently, the project has inconsistent usage of Prims when it comes to feeding strings to different types. Some Prims are given only the part of the string they specifically need, while others are fed the entire string and expected to parse it themselves. This inconsistency needs to be resolved.

We should always feed the entire string to all types of Prims.

Implement Custom Error Classes and Structured Error Handling Across the Project

For code maintainability and debugging sake, there's a need to adopt a more structured approach to error handling. The approach is to create custom error classes tailored to specific error types that are most likely to occur in the project. These custom errors should be caught and handled using try/catch blocks to clearly segregate the error-handling logic from the core functionality.

Create and Deploy a Presentation Prototype

Description:

Develop a presentation prototype using the current version of the framework. The prototype will serve multiple purposes: to gather initial user feedback, to demonstrate the framework's capabilities and limitations, and to evaluate the framework's aesthetic results. Deploy the prototype using GitHub Pages.

Steps:

  1. Define the objectives and key features of the prototype.
  2. Develop the prototype within the framework.
  3. Test the prototype.
  4. Deploy the prototype using GitHub Pages.
  5. Gather feedback for future development.

Implement Self-Seeding of Prims and Refactor Sprite.sow() Method

The current implementation of the interaction between Sprite and Production has two key issues that need to be addressed:

Issue 1: Lack of Self-Seeding for Missing Prims

During the implantation step (Sprite.implant()), if the required Prims are not found in the Production, the system throws an error instead of attempting to self-seed the missing Prims. The desired behavior is to identify these missing Prims and prepare them for addition to the Production or another targeted Production via the Sprite.sow() method.

##Expected Behavior:

If a required Prim is not found during implantation, it should be prepared for self-seeding in Sprite.sow().

##Current Behavior:

The system throws an error if a required Prim is not found.

####Issue 2: Hardcoded Prims in Sprite.sow() Method
Currently, the Prims to be seeded are hardcoded within the Sprite.sow() method, as seen in this snippet:
return [{ targets: [ this.targetGlyph ], prim: new ParameterPrim() }];

This approach does not account for conditional seeding of Prims based on the earlier check in the implantation step.

##Expected Behavior:

The Sprite.sow() method should dynamically generate Prims based on the checks performed in the Sprite.implant() method.

##Current Behavior:

Prims are hardcoded, making it inflexible for different conditions.

##Solution

  1. Sprite.sow() should return a reference to an object where the needed Prims are listed and mapped to their targets.
  2. See the most light and efficient way to do this.

Update Sprite parameters in real time.

Find a way to control Sprite parameters from the UI without requiring the re-instantiation of the Model.

Generating a new composition will be unavoidable and the change won't be smooth.

From a user point of view a regenerate button or mode will make sense.

It's important to make a distinction between the UI controls that:

A) Modify the drawing parameters: the changes are gradual (continuous) and smooth.

B) Modify the structure: the changes can be jumpy or flickery (discrete). They will also be computationally more expensive.

Evaluate L-System's Framework Adaptability in Different Projects

Assess the framework's adaptability by attempting to implement it in multiple projects, starting with Arborator and then FASS. This will test how versatile and reusable the library is.

Expected Outcome:

  1. Insights into how well the framework adapts to different project needs.
  2. Notes and thoughts for improving its adaptability and usefulness.

Steps:

  1. Choose different projects that currently use the L-System library ie. Arborator and FASS.
  2. Try to integrate the library into these projects.
  3. Note any friction or limitations.

Additional Criteria for Evaluation:

Aesthetic Versatility: Can the framework be easily adapted to achieve different visual styles and complexities?

Additional Steps:

Try to achieve various visual styles and document any limitations or difficulties.

Evaluate L-System Framework Based on a Test Design

Design specific test cases that are representative of typical tree-like and botanical structures. These test cases will serve as benchmarks for assessing how well the existing functionalities align with the project's objectives.

The aim is to evaluate the framework's current capabilities, ease of use for the developer and identify which features may need to be added or adjusted for greater adaptability.

Expected Outcome:

  1. A set of design cases for generating different types of tree-like structures.
  2. Summary about all the findings and thoughts.

Steps:

  1. Create design cases for generating various tree and plant forms.
  2. Use the library to try to achieve these designs.
  3. Take notes.

Additional Criteria for Evaluation

Aesthetic Quality: Assess the visual quality of the generated botanical structures.
Intuitiveness and Creativity: Evaluate how intuitively and creatively a creative/developer can work with the framework to achieve visually worthy results.

Refactor Sprite run() Method into Two Separate Methods for Parsing and Execution

The current flow merges both parameter parsing and glyph sequence processing into a single run() method within Sprite classes. Break down this down into two distinct methods for separation of responsibilities.

The update() method will be invoked first to handle parameter parsing according to each Sprite's unique requirements.

The run() method will then be responsible solely for processing the provided Glyph sequence.

Refactor Code for Modularity

Description

Refactor the existing framework code to adhere to the API specifications.

Steps

  1. Package the framework into reusable, well-defined modules.
  2. Update the code according to the API specifications.
  3. Review code for potential optimizations.

Implement Base Method in Sprite Classes for Input Glyph Validations

In order to ensure consistency and correctness, add a base protected method within all Sprite classes. This method should validate the input Glyph against both the dialect and the directory of the associated Production.

Currently, each Sprite class contains repetitive code for validating input Glyphs against the dialect and the directory of the Production. Let's stay DRY.

Create Basic API Documentation

Description

Develop basic API documentation to help internal developers understand how to use the library.

Steps

  1. Document each API method and usage.
  2. Provide brief examples for key methods.

Create a Sprite to reverse accumulation

When the generation is 'pulled' rather than 'pushed' ie. B -> ( A -> f )B as opposed to B -> B( A-> f) the Glyphs are written like a trail that is left behind leading to the product's accumulation values increasing along the written sequence order which is not desirable for all types of designs one might want to generate.

A Sprite implanted on the progenitor Production (B) that would reverse the accumulation values on their descendants (A) would potentially fix the problem.

Composer Fails to Generate Sequence for Axioms with More than 1 Glyph Symbol

Context:

Within our L-System architecture, the Composer is responsible for generating sequences based on a given axiom and set of Production Rules. Typically, an axiom is a single Glyph symbol (e.g., Y), which the Composer uses as a starting point to generate the sequences according to the Production Rules.

##Problem:
Currently, the Composer fails to initiate sequence generation when provided with an axiom longer than a single character (e.g., YB). This limitation significantly restricts the types of initial conditions that can be used in our L-System which would be very useful to use a starting point and save us both iteration processing and number of rules we need to define.

Expected Behavior:

The Composer should be capable of starting sequence generation from an axiom of any length, not just single-symbol axioms.

Symbols used in and by Prims shouldn't be managed by the Alphabet

The system has now the ability to handle parameter strings separately from production and command glyphs. As a result everything in between the '(' and ')' is a safe namespace. Moreover Prims should be responsible for the validations of their own identifiers and data.

Refactor update() Method to Parse Parameters in Base Production Class

"The current implementation of the update() method in each Production class involves parsing serialized parameter strings (params) to update or make use of the Prims. Given the similar logic across different Production classes, it would be beneficial to centralize this parsing logic in the base Production class.

Steps:

  1. Create an array in the base class to store fully-formed Prims.
  2. Implement the parsing logic in the update() method of the base class to populate this array with the appropriate Prims.
  3. Remove redundant parameter parsing logic from the update() methods in the derived classes.
  4. Test to ensure that everything keeps working as before.

Define API Specifications

Description

Outline the API design that the library will implement.

Steps

  1. Identify the core functionalities to expose.
  2. Design the API methods and their parameters.
  3. Document the expected behavior of each API method.

Sprite Fails to Process in Productions Without Self-Replicating Head Glyph

Context:

In our L-System implementation, each Production Rule is composed of:

Head Glyph: The Glyph representing the Production. (e.g., Y)
Rule: The sequence of Glyphs that gets written whenever the head Glyph is read by the Composer. (e.g., [-B] [+B] Y)
In this scenario, the head Glyph can also be part of the rule, enabling the main recursion mechanism of L-Systems.

Problem:

When applying a Sprite to a Production, it fails to process if the head Glyph is not part of the rule. This limitation restricts the usability of Sprites and prevents them from working with Productions that do not feature self-replication.

Expected Behavior:

A Sprite should be capable of being applied to any Production, irrespective of whether the head Glyph is part of the rule or not.

Steps to Reproduce:

  1. Create a Production with a head Glyph that is not part of the rule (e.g., Y -> [-B] [+B]).
  2. Apply a Sprite to this Production.
  3. Observe that the Sprite fails to process.

Possible cause:

Each progenitor Production overwrites the Prims of their descendent Productions replacing them with updated Prims.
If the Prim associated with the Sprite can't be added to any Glyph it won't be able to run again.

Where a possible solution could be found:

The part of the code where each Production re-assigns the Prims after Sprites being processed.

Refactor Sequence Interpretation out of the Composer Class

Decouple the sequence interpretation into drawing and concrete operations commands Composer class.

Steps:

  1. Document the roles and responsibilities the Composer class should have.

  2. Determine New Architecture: Decide where the interpretation and translation functionalities should reside. This might involve creating a new class or adding a function or method to an existing one.

  3. Refactor Composer: Remove sequence interpretation and drawing command translation functionalities from the Composer class.

  4. Implement New Class/Method together with its typings and integrate it in the overall framework so all the components make correct use of it.

  5. Test the Changes: Make sure that all work as before.

Improve README texts

There's too much 'stream of consciousness', reads like a journal. Be more concise.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.