Skip to content
John Horigan edited this page Sep 10, 2019 · 16 revisions

Introduction to Context Free

Context Free (and the command-line tool cfdg) is a digital art program that takes a description of an image and generates the image (as a bitmap, vector image, or movie). The description is in the form of a context free grammar and is called a cfdg file (aka context free design grammar). The typical use of a context free grammar is to analyze a sentence of symbols to see if they can reduce to some root symbol. Context Free turns this around by starting with a root symbol/shape (called the startshape) and using the grammar rules to elaborate this into a sentence of symbols/shapes (squares, circles, triangles, fills, or user-defined).

A cfdg file typically consists of the startshape and some number of shape rules. The rules tell Context Free how to draw a shape in terms of other shapes (or primitive shapes). When Context Free draws a shape it remembers some state information for the shape: color, geometry (position, size, rotation, chirality, and skew), and Z order (for overlapping shapes). To draw the shape Context Free evaluates a rule for the shape and replaces the shape with the shapes listed in the rule. The state for these replacement shapes is relative to the state of the parent shape.

In addition to startshape and rules a cfdg file can contain a few other things: paths (user-defined primitive shapes), global variable definitions, user-defined functions, and configuration statements. A cfdg file can also import the contents of another cfdg file.

Version 2 of Context Free

This page describes the current version of Context Free. But you will encounter cfdg files that look different, but still execute in Context Free. These files are from Context Free 2.2 and earlier and have a different syntax. The Version 2 Syntax page describes how to understand these older files and provides translation tools.

Shape Rules

A shape rule describes how to draw a shape in terms of other shapes, called shape replacements. The replacement shapes can be recursive invocations to the same shape, invocations of other shapes that have rules, or primitive shapes. When a rule is executed the end result is some number of shapes that require further execution and some number of primitive shapes that are placed in the shape sentence. Here is some example code by Chris Coyne:

// Chris Coyne, copyright 2006
// Creative Commons licensed: Creative Commons Attribution 3.0 Unported
startshape blah
CF::Background = [hue 120 sat 1 b -0.5]
CF::MinimumSize = 0.1

shape blah {
 blah2 [alpha -1]
 blah2 [flip 90 alpha -1 x 5 b 1]
 blah2 [alpha -1 y -5 b 1]
 blah2 [flip 90 alpha -1 x 5 y -5]
}
 
shape blah2 {
 SQUARE []
 blah2 [alpha 0.0001 r 9..10 x 1 s 0.9995]
}

Shape blah is defined in terms of four blah2 shapes. Shape blah2 is recursively defined and draws a square primitive shape on each iteration.

Shape Adjustments

In the above example there was some text inside square brackets following each shape replacement. These are called shape adjustments. When a shape is executed it has some information associated with it, called its state: its geometry, its color, its z-position, and its lifetime (used in animation). This information passes to the shape replacements, but it is modified by the shape adjustment. Let's break down

blah2 [x 5 flip 90 alpha -0.25 b 0.5]

x 5 adjusts the geometry by shifting right 5 units. flip 90 changes the geometry by reflecting the blah2 shape across the y-axis (90°). alpha -0.25 changes the color to 25% more transparent. And b 0.5 adjusts the color to 50% brighter. The reflection and shift are performed relative to the geometry of the parent shape, not in any absolute reference frame. The alpha and brightness changes are performed relative to the alpha and brightness of the parent shape.

The geometry is stored as an affine transform matrix and all geometric adjustments are affine transformations. Color is stored as hue, saturation, brightness, and alpha. Color adjustments change one of these four components. Z state is stored as z position and z scale. Lifetime is stored as birth and death time and time scale.

StartShape

The startshape directive tells Context Free the root shape for the design. If the shape takes parameters then they must be specified in the startshape directive. The shape is initialized with the default shape state: opaque black (h/s/b/a = 0/0/0/1) and identity transforms for geometry, z, and lifetime. You have the option of putting a shape adjustment after the shape name in the startshape directive. If there is a shape adjustment then the default state is adjusted with it before executing the shape.

Expressions

Anywhere that you can put a number, you can put a numeric expression. The only exception is rule weights, which must be a numeric constant. Inside shape adjustments you can only use a subset of the expressions, called simple expressions.

Expressions support your standard +-*/ operators, parentheses, functions, comparison operators, boolean operators, and random operators.

Variables

Variables can be declared anywhere outside of shapes or inside of shapes. Global variables are computed at the start of the cfdg execution and can be referenced anywhere following their declaration. Variables local to a shape (or path) are computed each time the shape is executed, so they can be random or depend on shape parameters. If you want to have a global variable that is recomputed each time it is referenced then declare a user function with no arguments instead.

phi = (1 + sqrt(5)) / 2

Variables cannot be modified. In this code

count = 5
count = count - 1

The second count declaration does not modify the first count variable. Instead it declares a new count variable that renders the first one invisible. If the first count variable was global and the second count variable was local then the global count would be invisible until the local count went out of scope. Note that the scope of the second count variable begins after the count - 1 expression, which means that the count in the count - 1 expression refers to the first count variable.

User Functions

In addition to the predefined functions you can define your own. They can simply be interesting functions that we forgot:

erf(n) = if(n > 0, 1, -1) * sqrt(1 - exp(-n * n * (4 / 3.1415926535 + 0.147 * n * n) / (1 + 0.147 * n * n)))

or they can be complicated, recursive, functional programming:

// Ackerman function
ack(m, n) = 
    if(m == 0, 
        n+1, 
        if(n == 0, 
            ack(m-1, 1), 
            ack(m-1, ack(m, n-1))
        )
    )

Paths

Path declaration create new primitive shapes. The contents of a path declaration are a series of path operations (line segments, arcs, and bezier curves) and drawing commands (stroke or fill a path).

path box {
    MOVETO( 0.5,  0.5)
    LINETO(-0.5,  0.5)
    LINETO(-0.5, -0.5)
    LINETO( 0.5, -0.5)
    CLOSEPOLY()     // go back to (0.5, 0.5) and close the path
    STROKE()[]      // draw a line on the path using the default line width (10%)
}

Control Structures

Shape rules and paths can have control structures that affect how the shape replacements or path elements are executed. These control structures are loops, if statements, switch statements, transform statements, and clone statements.

Loops

path heptstar {
	MOVETO(cos (90 - 720 / 7), sin(90 - 720 / 7))
	loop 6 [r (720/7)]
		LINETO(0, 1)
	CLOSEPOLY()
	FILL(CF::EvenOdd)[]
}

In this path the LINETO path operation is executed 6 times in a loop. The shape adjustment following the 6 is called the loop adjustment. The world state for the loop body is adjusted with the loop adjustment following each iteration of the loop. So the first iteration of the loop the LINETO is executed in the path world state. The second iteration is with the path world state rotated by 102.857°. The third iteration is with the path world state rotated by 205.714°, and so on.

If Statements

An if statement makes the execution of a block of shape replacements or path elements dependent on whether an expression is true (≠0) or false (=0).

path flower(number petals, number filled)
{
  MOVETO(cos(-180/petals), sin(-180/petals))
  loop petals [r (360/petals)]
    ARCTO(cos(180/petals), sin(180/petals), 
          0.4, 0.4 + 0.2 * (petals - 5), 90)
  CLOSEPOLY(CF::Align)
  if (filled) FILL[a -0.5]
  MOVETO(0.65, 0)
  ARCTO(-0.65, 0, 0.65, CF::ArcCW)
  ARCTO( 0.65, 0, 0.65, CF::ArcCW)
  CLOSEPOLY()
  FILL[a -0.5]
}

In this path the center of the flower is filled only if the incoming fill parameter is non-zero. If statements can also have an else clause that is only executed if the if condition is false.

Switch Statements

A switch statement evaluates a selector expression and then picks one of several blocks of shape replacements or path elements to execute based on the selector value.

shape randShape {
  switch (randint(3)) {
    case 0:
      SQUARE []
    case 1:
      TRIANGLE []
    case 2: {
      CIRCLE []
    }
    else:
      FILL []  // never happens
  }
}

The selector value is rounded down to the next integer before the case block is selected. The case block indices are constant expressions that are also rounded down to the next integer.

Transform Statements

A transform statement takes a shape adjustment and executes a block of shape replacements or path elements with a world state that has been adjusted by it.

path allenKey {
  path CIRCLE[]
  transform [s 0.35] {
    MOVETO(cos(30), sin(30))
    loop 5 [r 60] LINETO(0, 1)
  }
  FILL(CF::EvenOdd)[]
}

Clone Statements

A clone statement behaves just like a transform statement except that the replacement shapes all look the same, even if they are random. This violates context-free purity, so you must set CF::Impure.

CF::Impure = 1
 
shape flake {
    clone CF::Dihedral, 6
    {
        SPIKE []
    }
}

Shape/Path Parameters

In addition to the state that a parent shape sends to child shape in a shape replacement it can also send parameters. These parameters can be numbers, shape adjustments, or shape specifications.

shape curve(number shrink, number turn)
{
  SQUARE []
  curve(=) [[y 0.5 r turn s shrink y 0.5]]
}

A shape specification for shape curve includes two numeric parameters, that are bound to the variables shrink and turn when curve is executed.

shape grass {
  loop 100 [x 1]
    curve(0.975, 0±2)[]
}

Importing CFDG Files

A cfdg file can insert the contents of another cfdg file using an import directive. The shapes, paths, variables, and user functions declared in the imported cfdg file are available following the import directive. They can also be inserted in a separate namespace if there are naming conflicts between the imported file and the importing file.

import@font alfreebet.cfdg
 
shape woot {
  font::WW[]
  font::OO[x 1.3]
  font::OO[x 2.4]
  font::TT[x 3.5]
}

Configuring Context Free/cfdg

Context Free's behavior can be changed by setting special configuration variables in the CF:: namespace. Most of these are fairly obscure except for the four below.

Background Color

The default background color is opaque white (hue = 0, sat = 0, b = 1, a = 1). Assigning a shape adjustment to CF::Background will change the background color accordingly.

CF::Background = [hue 163 b -.875 saturation .75]  // murky green

Tiling and Symmetry

Tiled rendering (and frieze rendering) is disabled by default, but is enabled and configured by assigning a shape adjustment to CF::Tile.

CF::Tile = [s 70]

Symmetry operations are enabled and configured by assigning a list of symmetry specifications to CF::Symmetry.

CF::Symmetry = CF::Dihedral, 6   // snowflake symmetry

Size

By default the drawing canvas is dynamically sized to fit all of the shapes drawn. Dynamic sizing of the canvas is disabled by assigning a shape adjustment to CF::Size. The logical size and position of the canvas is set by the adjustment.

CF::Size = [s width height x xpos y ypos] // center on (-xpos, -ypos)