Noisemaker Composer

Noisemaker Composer is a high-level interface for creating generative art with procedural noise.

Presets are authored with the Composer DSL, a domain-specific language for defining procedural art presets. It provides a structured, declarative syntax for composing noise generators and image effects while supporting randomization, inheritance, and reusable configurations.

Note

JavaScript Compatibility: The Composer DSL is shared between Python and JavaScript implementations. See JavaScript API for using presets in the browser.

Note

Two DSLs: Noisemaker has two distinct domain-specific languages:

  • Composer DSL (this document) — A declarative DSL for CPU-based compositions using Python/JavaScript effects. Presets describe what to generate via layered configurations.

  • Polymorphic DSL — An imperative DSL for GPU-based shader compositions. See Polymorphic DSL for details.

Overview

The Composer DSL allows you to define presets as JSON-like objects with specialized syntax extensions. Each preset can:

  • Layer (inherit from) other presets

  • Define reusable settings with random values

  • Configure noise generation parameters

  • Apply effects per-octave, post-processing, and final passes

  • Reference other presets inline

The canonical preset library lives in share/dsl/presets.dsl and is parsed by noisemaker.dsl. Both the reference Python implementation and the JavaScript port consume that file, so any change to the DSL immediately applies to both environments.

Custom Presets

You can create your own presets in a separate DSL file and use them with the CLI:

# Use a custom presets file
noisemaker generate my-preset --presets ~/my-presets.dsl -o custom.png

Custom preset files follow the same DSL syntax as the built-in presets.dsl. You can reference the built-in file as a template. Custom presets can layer on top of each other but cannot inherit from the built-in presets unless they are included in the same file.

For programmatic usage in Python:

from noisemaker.presets import set_presets_path, Preset

# Set custom presets file
set_presets_path('/path/to/my-presets.dsl')

# Now use presets from the custom file
preset = Preset('my-custom-preset')
preset.render(seed=42, shape=[1024, 1024, 3], filename='output.png')

Philosophy

“Composer” presets expose Noisemaker’s lower-level generator and effect APIs. The DSL keeps the data model compact while remaining expressive enough to describe the same building blocks the original Python dictionaries exposed.

At a high level, each preset answers five key questions:

  1. Which presets are being built on? (layers)

  2. What are the meaningful variables? (settings)

  3. What are the noise generation parameters? (generator)

  4. Which effects should be applied to each octave? (octaves)

  5. Which effects should be applied after flattening layers? (post and final)

Grammar

The DSL follows a strict grammar for parsing and validation. Programs are parsed into an abstract syntax tree (AST) and evaluated against a whitelist of operations and surfaces—the evaluator never invokes eval or Function.

Program        ::= Chain ('.' 'out' '(' OutputRef? ')' )?
Chain          ::= Expr ('.' Call)*
Expr           ::= Ident '(' ArgList? ')'
Call           ::= Ident '(' ArgList? ')'
ArgList        ::= Arg (',' Arg)* ','?
Arg            ::= NumberExpr | String | Boolean | Color
                 | Ident | Enum | OutputRef | SourceRef | Null
                 | List | Dict
NumberExpr     ::= Primary
                 | NumberExpr ( '+' | '-' | '*' | '/' ) NumberExpr
                 | NumberExpr '?' NumberExpr ':' NumberExpr
Primary        ::= Number | Boolean | Null | 'Math.PI'
                 | Ident | Enum | Call | MemberExpr
                 | '(' NumberExpr ')'
Call           ::= (Ident | MemberExpr) '(' ArgList? ')'
MemberExpr     ::= Ident '.' Ident
List           ::= '[' ArgList? ']'
Dict           ::= '{' (DictEntry (',' DictEntry)* ','?)? '}'
DictEntry      ::= (String | Ident) ':' Arg
Enum           ::= Ident '.' Ident
OutputRef      ::= 'o' Digit
Ident          ::= Letter ( Letter | Digit | '_' )*
Number         ::= Digit+ ('.' Digit+)?
String         ::= '"' [^"\n]* '"'
Digit          ::= '0'…'9'
Letter         ::= 'A'…'Z' | 'a'…'z'
Boolean        ::= 'true' | 'false'
Null           ::= 'null'
Color          ::= '#' HexDigit HexDigit HexDigit
                 (HexDigit HexDigit HexDigit)?
HexDigit       ::= Digit | 'A'…'F' | 'a'…'f'

Grammar Notes

  • Floats may omit the leading zero (e.g., .5).

  • Trailing commas in lists, dictionaries, and argument lists are allowed.

  • Enum resolves to JavaScript enum objects exported from constants.js.

  • Whitespace and comments are skipped between tokens.

  • Single-line comments use // and run until the newline.

  • Block comments use /* */ and must be terminated before end of file.

Naming Conventions

  • Preset names use kebab-case (e.g., "acid-wash").

  • Preset setting keys use snake_case (e.g., bloom_alpha).

  • Function wrappers and argument keys use snake_case (e.g., random_int).

  • Enum wrapper members use snake_case (e.g., ColorSpace.rgb).

  • Underlying implementations:

    • Python enums and functions use snake_case.

    • JavaScript enums and functions use camelCase.

Preset Keys

Each preset is a dictionary that can contain the following top-level keys:

layers

Type: Array of strings

Purpose: Inherit settings, generators, and effects from parent presets.

Parent presets are applied in order, with later presets overriding earlier ones. The current preset’s settings override all parents.

layers: ["basic", "voronoi", "grain"]

settings

Type: Dictionary

Purpose: Define variables that can be referenced throughout the preset using settings.key_name.

Settings support literals, enum references, helper functions, arithmetic expressions, and conditionals.

settings: {
  freq: random_int(10, 15),
  octaves: 8,
  reindex_range: 1.25 + random() * 1.25,
  color_space: ColorSpace.rgb,
}

This example from the acid preset shows typical settings usage.

generator

Type: Dictionary

Purpose: Configure noise generation parameters passed to noisemaker.generators.multires.

generator: {
  freq: settings.freq,
  octaves: settings.octaves,
  lattice_drift: 1.0,
}

All keys must be valid generator parameters. Common ones include freq, octaves, ridges, distrib, color_space, hue_range, lattice_drift, corners, and spline_order.

octaves

Type: Array of effect calls

Purpose: Effects applied to each octave of noise during generation.

octaves: [
  derivative(alpha: 0.333),
]

post

Type: Array of effect calls and/or preset references

Purpose: Effects applied after noise octaves are combined.

post: [
  bloom(alpha: 0.25),
  saturation(amount: 1.5),
]

final

Type: Array of effect calls and/or preset references

Purpose: Final effects applied after all post-processing.

final: [
  aberration(displacement: 0.01),
]

unique

Type: Boolean

Purpose: Mark preset as unique (not for general layering). Defaults to false.

unique: true

Data Types

The DSL supports the following data types:

Numbers

Integers and floats, including arithmetic expressions:

freq: 5
alpha: 0.5 + random() * 0.25
computed: settings.base * 2 + 1

Strings

Double-quoted strings (no escape sequences):

palette_name: "viridis"

Booleans

Keywords true and false:

ridges: true
inverse: false

Null

Keyword null:

mask: null

Arrays

Lists of values:

freq: [4, 8]
layers: ["basic", "grain"]
options: [1, 2, 3]

Dictionaries

Key-value pairs:

settings: {
  key1: value1,
  key2: value2,
}

Enums

Access enum members using dot notation:

color_space: ColorSpace.rgb
mask: ValueMask.chess
dist_metric: DistanceMetric.euclidean

Available enums include: ColorSpace, ValueDistribution, ValueMask, DistanceMetric, VoronoiDiagramType, PointDistribution, InterpolationType, OctaveBlending, WormBehavior, and more.

Expressions

Arithmetic

Standard operators: +, -, *, /

reindex_range: 1.25 + random() * 1.25
double_freq: settings.freq * 2

Conditional (Ternary)

JavaScript-style ternary:

voronoi_inverse: coin_flip() ? true : false
freq: random() < 0.5 ? 4 : 8

Comparison and Logic

Comparison operators: <, >, <=, >=, ==, !=

Logical operators: && (and), || (or)

use_effect: random() < 0.75

Settings References

Access previously defined settings:

settings: {
  freq: random_int(8, 12),
  reflect_range: 7.5 + random() * 3.5,
}

Helper Functions

The DSL provides built-in helper functions for randomization and utilities:

random()

Returns a random float between 0.0 and 1.0.

bloom_alpha: 0.025 + random() * 0.0125

random_int(min, max)

Returns a random integer between min and max (inclusive).

freq: random_int(10, 15)
voronoi_sdf_sides: random_int(2, 8)

coin_flip()

Returns a random boolean (true or false).

voronoi_inverse: coin_flip()

random_member(collection)

Returns a random member from a single iterable. The iterable can be an enum (for example, DistanceMetric) or any collection that yields members. Passing scalars directly raises an error.

dist_metric: random_member(DistanceMetric.all())

voronoi_diagram_type: random_member([
  VoronoiDiagramType.range,
  VoronoiDiagramType.color_range,
  VoronoiDiagramType.regions,
])

enum_range(start, end)

Returns a list of enum members from start to end (inclusive). Both arguments must be members of the same enum.

values: enum_range(InterpolationType.constant, InterpolationType.bicubic)
// [InterpolationType.constant, InterpolationType.linear,
//  InterpolationType.cosine, InterpolationType.bicubic]

stash(key, value)

Stores a value for later retrieval within the same evaluation context.

temp: stash("my_key", 42)
retrieved: stash("my_key")

mask_freq(mask, repeat)

Returns the appropriate frequency for a given mask and repeat value.

freq: mask_freq(ValueMask.chess, 8)

preset(name)

Inline another preset’s post/final effects.

post: [
  bloom(alpha: 0.25),
  preset("grain"),
]

Enum Helper Methods

Enums provide helper methods to filter and retrieve specific members:

EnumType.all()

Returns all enum members.

all_colors: ColorSpace.all()

Specific Enum Methods

Different enums provide specialized filter methods:

ColorSpace:

  • ColorSpace.color_members() - Color spaces only

DistanceMetric:

  • DistanceMetric.absolute_members() - Absolute metrics

  • DistanceMetric.all() - All metrics

ValueMask:

  • ValueMask.procedural_members() - Procedural masks

  • ValueMask.grid_members() - Grid-based masks

  • ValueMask.glyph_members() - Glyph/character masks

  • ValueMask.nonprocedural_members() - Non-procedural masks

  • ValueMask.rgb_members() - RGB-based masks

PointDistribution:

  • PointDistribution.circular_members() - Circular distributions

  • PointDistribution.grid_members() - Grid-based distributions

WormBehavior:

  • WormBehavior.all() - All worm behaviors

Example usage:

dist_metric: random_member(DistanceMetric.all())
color_space: random_member(ColorSpace.color_members())
mask: random_member(ValueMask.grid_members())

Effect Calls

Effects are called with named parameters using colon syntax:

effect_name(param1: value1, param2: value2)

Examples from actual presets:

octaves: [
  derivative(alpha: 0.333),
]

post: [
  bloom(alpha: settings.bloom_alpha),
  saturation(amount: 1.5),
]

final: [
  aberration(displacement: 0.01),
]

Complete Example

Here’s the acid preset from the standard library (one of many presets in share/dsl/presets.dsl):

{
  "acid": {
    layers: ["basic", "reindex-post", "normalize"],
    settings: {
      color_space: ColorSpace.rgb,
      freq: random_int(10, 15),
      octaves: 8,
      reindex_range: 1.25 + random() * 1.25,
    },
  },

  // ... hundreds of other presets ...
}

This preset demonstrates:

  • Layering: Inherits from basic (noise generation), reindex-post (color reindexing effect), and normalize (value normalization)

  • Settings: Defines randomized frequency (10-15), fixed octave count (8), and randomized reindex range

  • Enums: Uses ColorSpace.rgb for RGB color space

  • Randomization: Combines random_int() and random() for varied output

Another example, acid-droplets, shows a more complex preset:

{
  "acid-droplets": {
    layers: [
      "multires",
      "reflect-octaves",
      "density-map",
      "random-hue",
      "bloom",
      "shadow",
      "saturation"
    ],
    settings: {
      freq: random_int(8, 12),
      hue_range: 0,
      lattice_drift: 1.0,
      mask: ValueMask.sparse,
      mask_static: true,
      palette_on: false,
      reflect_range: 7.5 + random() * 3.5,
    },
  },
}

This demonstrates extensive layering of multiple effect presets to create a complex composition.

Naming Conventions

The DSL follows these naming conventions:

  • Preset names: kebab-case (e.g., "my-awesome-preset")

  • Setting keys: snake_case (e.g., base_freq, bloom_alpha)

  • Function names: snake_case (e.g., random_int, coin_flip)

  • Enum types: PascalCase (e.g., ColorSpace, ValueMask)

  • Enum members: snake_case (e.g., ColorSpace.rgb, ValueMask.chess)

Best Practices

  1. Use settings for reusable values

    settings: {
      bloom_alpha: 0.025 + random() * 0.0125,
    },
    final: [
      bloom(alpha: settings.bloom_alpha),
    ]
    
  2. Layer presets for composition

    layers: ["basic", "reindex-post", "normalize"]
    
  3. Use descriptive setting names

    reflect_range: 7.5 + random() * 3.5  // Good
    rr: 7.5 + random() * 3.5              // Bad
    
  4. Reference the canonical library

    Study existing presets in share/dsl/presets.dsl for patterns and techniques.

Debugging

When a preset doesn’t parse or evaluate correctly:

  1. Check syntax: Ensure all braces, brackets, and parentheses are balanced

  2. Verify enum names: Enum references must exactly match defined enums

  3. Check parameter names: Effect parameters must match the effect’s signature

  4. Look for typos: Setting references must exactly match defined keys

  5. Test incrementally: Build complex presets step-by-step

The Python and JavaScript parsers provide error messages with line/column information when syntax errors occur.

Using Presets

Note

JavaScript examples assume an ES module environment (for example, <script type="module">) so that await can be used at the top level.

Basic Usage

Python

from noisemaker.composer import Preset

preset = Preset('acid')
preset.render(seed=1, shape=[1024, 1024, 3], filename='acid.png')

JavaScript

import { PRESETS } from './js/noisemaker/presets.js';
import { Preset } from './js/noisemaker/composer.js';

const presets = PRESETS();
const preset = new Preset('acid', presets);
await preset.render(1, {
  width: 1024,
  height: 1024,
  canvas: document.getElementById('output')
});

Working with Arrays

Python

from noisemaker.composer import Preset

preset = Preset('voronoi')
tensor = preset.render(seed=42, shape=[512, 512, 3])
array = tensor.numpy()

JavaScript

import { PRESETS } from './js/noisemaker/presets.js';
import { Preset } from './js/noisemaker/composer.js';

const presets = PRESETS();
const preset = new Preset('voronoi', presets);
const tensor = await preset.render(42, { width: 512, height: 512 });
const array = tensor.read();

Override Settings

Python

from noisemaker.composer import Preset

preset = Preset('acid', settings={'freq': 20, 'octaves': 12})
preset.render(seed=1, shape=[1024, 1024, 3], filename='custom.png')

JavaScript

import { PRESETS } from './js/noisemaker/presets.js';
import { Preset } from './js/noisemaker/composer.js';

const presets = PRESETS();
const preset = new Preset('acid', presets, { freq: 20, octaves: 12 });
await preset.render(1, { width: 1024, height: 1024 });

List Available Presets

Python

from noisemaker.presets import PRESETS

presets = PRESETS()
print(list(presets.keys()))

JavaScript

import { PRESETS } from './js/noisemaker/presets.js';

const presets = PRESETS();
console.log(Object.keys(presets));

Architecture Overview

The Noisemaker Composer system is built on three layers:

  1. DSL Layer (noisemaker.dsl)

    Parses and evaluates the Composer DSL from share/dsl/presets.dsl. The same DSL file is used by both Python and JavaScript implementations, ensuring cross-platform consistency.

  2. Preset Layer (noisemaker.presets, noisemaker.composer)

    Loads preset definitions and provides the Preset class for rendering. Handles preset inheritance (layering), settings resolution, and effect application.

  3. Generator/Effect Layer (noisemaker.generators, noisemaker.effects)

    Low-level TensorFlow operations for generating procedural noise and applying image effects.

The DSL provides a declarative interface to these lower-level APIs, making it easy to compose complex generative art without writing imperative code.

Cross-Platform Compatibility

Both the Python and JavaScript implementations:

  • Parse the same DSL file (share/dsl/presets.dsl)

  • Use identical tokenizer, parser, and evaluator logic

  • Produce deterministic output given the same seed

  • Support the same set of helper functions and enums

Any change to the DSL immediately applies to both environments, making it easy to maintain consistency across platforms.

See Also