CurveScript

CurveScript is a language for programmatically generating vector graphics which may then be serialized to various formats such as SVG, XAML, and PDF.

CurveScript programs compile to a simple platform-independent instruction set that can be executed by an equally simple virtual machine. At the time of writing, CurveScript has 19 instructions, and the VM is implemented in just under 200 lines of Go. This simplicity makes it easy for applications to include a VM, which allows them to include resources as compiled CurveScript programs rather than fully rendered SVG/XAML/PDF, which may require significantly more storage.

If you're not interested in the details and just want to see it in action, head over to the online IDE.

Guide

The language is far from finalized, and will likely change as I use it more. Because of this, the guide is in very rough shape, favoring easy updating over thoroughness as the language changes. The guide will probably only be of much use to people already familiar with other languages.

General Behavior

Expressions

array

An array is a reference to a sequence of elements of the same type. Each element is identified by its 0-based position in the sequence. Arrays are instantiated by enclosing elements in brackets. Array types are specified by enclosing the element type in square brackets.

secondElement := function(elements [number]|number) {
    return(elements[1])
}

radii := [-1.5,42,-100]
radius := secondElement(radii)

shape := circle(radius)
return(translate(shape, 100, 100))

assignment

Values can be assigned to named storage locations using the equal sign between the name of the storage location and the value to be stored. If the storage location needs to be allocated, the equal sign is prefixed with a colon.

radius := -10
radius = 50

shape := circle(radius)
return(translate(shape, 100, 100))

If a function call returns multiple values, multiple comma-separated storage locations are used.

doubleTriple := function(value number|number, number) {
    return(value * 2, value * 3)
}

width, height := doubleTriple(10)
shape := rectangle(width, height)
return(translate(shape, 100, 100))

block

Blocks contain a sequence of statements, including sub-blocks. Blocks are used to hold the body of functions and conditionals. Blocks can also be used to restrict the scope of a variable.

radius := -5
{
    factor := -6
    radius = radius * factor
}

shape := circle(radius)
return(translate(shape, 100, 100))

boolean

Boolean is a built-in type with only two possible values: true, and false.

wide := true

width := 40
height := 40

if wide {
    width = width * 2
}

shape := rectangle(width, height)
return(translate(shape, 100, 100))

call

Functions can be called using a pair of parentheses that contain one comma-separated argument for each parameter of the function. The call evaluates to the set of values returned by the called function.

swap := function(first number, second number|number, number) {
    return(second, first)
}

portraitWidth := 10
portraitHeight := 20

landscapeWidth, landscapeHeight := swap(portraitWidth, portraitHeight)

shape := rectangle(landscapeWidth, landscapeHeight)
return(translate(shape, 100, 100))

function

Functions are defined with the function keyword, followed by parentheses containing the inputs and outputs, followed by a block containing the implementation.

The inputs are a comma-separated list of identifier and type pairs. The outputs are a comma-separated list of types. The input list and output list are separated by a vertical bar.

swap := function(first number, second number|number, number) {
    return(second, first)
}

portraitWidth := 10
portraitHeight := 20

landscapeWidth, landscapeHeight := swap(portraitWidth, portraitHeight)

shape := rectangle(landscapeWidth, landscapeHeight)
return(translate(shape, 100, 100))

if

Blocks can be conditionally executed with the if keyword, followed by any expression that evaluates to the boolean, followed by the block to execute if the condition evaluates to true.

Additional conditions can be added with an "else if". Only a single block within a chain will be executed (the first block whose expression evaluates to true).

Finally, a block can be execute if none of the conditions are met by prefixing it with the "else" keyword.

width := 10
height := 20
if 25 < 10 {
    width = 1
} else if 15 < 10 {
    width = 10
} else if 5 < 10 {
    width = 20
} else {
    width = 30
}

shape := rectangle(width, height)
return(translate(shape, 100, 100))

infix operators

Infix operators combine two expressions and produce a single value. The following operators are defined:

height := 20
width := 0
if 1 < 2 {
    width = width + 1
}
if 1 > 2 {
    width = 0
}
if 1 == 2 {
    width = 0
}
if 1 != 2 {
    width = width + 2
}
width = width + 3
width = width - 1
width = width * 40
width = width / 10

shape := rectangle(width, height)
return(translate(shape, 100, 100))

number

The number type represents a 64-bit double-precision floating point value (IEEE754). Number literals must begin with a digit.

prefix operator

Prefix operators convert the value from a single following expression into a different value. The following prefix operators are defined:

height := 20
condition := false
width := -20

if !condition {
    width = -width
}

shape := rectangle(width, height)
return(translate(shape, 100, 100))

return

The return keyword exits the current function and makes the function call evaluate to the specified parameters. The return must have the same number and type of parameters specified by the function being returned from.

size := function(top number, right number, bottom number, left number|number, number) {
    return(bottom - top, right - left)
}

width, height := size(12, 250, 30, 100)

shape := rectangle(width, height)
return(translate(shape, 100, 100))

while

The while loop allows a block of code to be repeated while a condition evaluates to true.

height := 20
width := 0

iterations := 5
while iterations > 0 {
    width = width + 4
    iterations = iterations - 1
}

shape := rectangle(width, height)
return(translate(shape, 100, 100))

Standard Library Concepts

curvescript provides APIs for manipulating various graphics objects:

curve: a cubic bezier curve.

contour: a start point, followed by a continuous sequence of curves, each starting where the previous ended.

shape: a set of contours that define a filled area. Overlapping/enclosed contours form voids (e.g. a 2D ring has two concentric circular contours). Non-overlapping contours form disjoint areas within a single shape. Contour order is visually irrelevant, as they are all combined to describe a filled area.

group: a sequence of elements, each of which may be a shape or another group. The order of nodes within the sequence determines the rendering order, with the content of earlier elements appearing underneath later elements.

A shape can be used in any situation that calls for a group.

The ultimate purpose of a curvescript program is to use the APIs to create and return a group that describe the desired image. This content of this group is rendered into the desired image format (e.g. SVG).

curvescript uses a coordinate system starting at the top-left corner, with increasing x and y coordinates towards the bottom-right.

Standard Library API

oval(width number, height number|shape)
creates a new shape containing an oval of the specified width and height.

circle(radius number|shape)
creates a new shape containing a circle of the specified radius.

rectangle(width number, height number|shape)
creates a new shape containing a rectangle of the specified width and height.

roundedRectangle(width number, height number, radius number|shape)
creates a new shape containing a rectangle of the specified width and height, with corners rounded to the specified radius.

translate(group group, xOffset number, yOffset number|)
moves the specified group xOffset units horizontally, and yOffset units vertically.

rotate(group group, degrees number|)
rotates the specified group about the origin, the specified number of degrees.

scale(group group, xScale number, yScale number|)
scales the specified group about the origin, horizontally by the factor specified in xScale, and vertically by the factor specified in yScale.

merge(first shape, second shape|shape)
creates a new shape consisting of all area included in either the first or second shape.

cut(first shape, second shape|shape)
creates a new shape consisting of all area included in the first shape that does not overlap with the second shape.

overlap(first shape, second shape|shape)
creates a new shape consisting of all area included in both the first shape and second shape.

xor(first shape, second shape|shape)
creates a new shape consisting of all area included in either the first shape or second shape, but not both.

emptyShape(|shape)
creates a new shape containing no contourss.

startContour(shape shape, x number, y number|)
creates a new contour within the specified shape, beginning at the specified x and y coordinates.

absoluteLine(shape shape, x number, y number|)
extends the last contour in the specified shape with a line connecting the last point in the shape to the specified x and y coordinates.

absoluteCurve(shape shape, endX number, endY number, startControlOffsetX number, startControlOffsetY number, endControlOffsetX number, endControlOffsetY number)
extends the last contour in the specified shape with a curve connecting the last point in the contour to the specified endX and endY coordinates. The curvature is determined by a control point extending from the start of the curve, and a control point extending from the end of the curve. The start control point is specified as an offset from the start point, via startControlOffsetX and startControlOffsetY. The end control point is specified as an offset from the end point, via endControlOffsetX and endControlOffsetY.

absoluteArc(shape shape, endX number, endY number, startDirectionX number, startDirectionY number|)
extends the last contour in the specified shape with a circular arc connecting the last point in the contour to the specified endX and endY coordinates. The arc begins in the direction specified by startDirectionX and startDirectionY (their magnitude does not matter, as the direction and endpoint imply the necessary radius the arc).

line(shape shape, endOffsetX number, endOffsetY number|)
extends the last contour in the specified shape with a line connecting the last point in the contour to the point endOffsetX and endOffsetY units away.

curve(shape shape, endOffsetX number, endOffsetY number, startControlAngle number, startControlLength number, endControlAngle number, endControlLength number|)
extends the last contour in the specified shape with a curve connecting the last point in the contour to a point endOffsetX away horizontally, and endOffsetY away vertically. The curvature is determined by a control point extending from the start of the curve, and a control point extending from the end of the curve. The start control point is startControlLength away from the start point, with an angle of startControlAngle degrees between the start control point and the end point. The end control point is endControlLength away from the end point, with an angle of endControlAngle degrees between the end control point and the start point.

arc(shape shape, startDirectionX number, startDirectionY number, angle number, radius number|)
extends the last contour in the specified shape with a circular arc starting in the direction specified by startDirectionX and startDirectionY, circumscribing angle degrees, with the specified radius. The magnitude of startDirectionX and startDirectionY do not matter, as the radius is explicitly specified.

smoothLine(shape shape, length number|)
extends the last contour in the specified shape with a line of the specified length, continuing in the same direction as the end of the contour.

corner(shape shape, angle number, radius number|)
extends the last contour in the specified shape with a circular arc starting in the same direction as the end of the contour, circumscribing angle degrees with the specified radius.

smoothCurve(shape shape, endOffsetX number, endOffsetY number, startControlLength number, endControlAngle number, endControlLength number|)
extends the last contour in the specified shape with a curve connecting the last point in the contour to a point endOffsetX away horizontally, and endOffsetY away vertically. The curvature is determined by a control point extending from the start of the curve, and a control point extending from the end of the curve. The start control point is startControlLength away from the start point, continuing in the same direction as the end of the shape. The end control point is endControlLength away from the end point, with an angle of endControlAngle degrees between the end control point and the start point.

close(shape shape|)
extends the last contour in the specified shape with a line connecting the last point in the contour to the start point in the contour.

endOffset(shape shape|number, number)
returns the horizontal and vertical distance from the start of the last contour to the end of the last contour in the specified shape.

endPoint(shape shape|number, number)
returns the horizontal and vertical coordinates of the end of the last contour in the specified shape.

startPoint(shape shape|number, number)
returns the horizontal and vertical coordinates of the start of the last contour in the specified shape.

point(shape shape, curveNumber number|number number)
returns the horizontal and vertical coordinates of the end of the last curve indicated by curveNumber. Point 0 is the start of the contour, point 1 is the end of the first curve, point 2 is the end of the second curve, and so on.

symmetricControl(shape shape, endOffsetX number, endOffsetY number|number, number)
computes the angle and length of the starting control point required to make a symmetric curve from the end of the last contour in the specified shape to a curve ending endOffsetX and endOffsetY away from the end of the last contour.

bounds(group group|number, number, number, number)
returns the left, top, right, and bottom coordinates of a rectangle enclosing the specified group.

center(group group|group)
centers the specified group about the origin and returns the specified group.

trim(group group|group)
moves the top left corner of the specified group to the origin and returns the specified group.

fill(group group, hue number, saturation number, lightness number, opacity number|)
sets the fill of each shape in the specified group to the color with the specified hue, saturation, lightness, and sets the fill to the specified opacity.

stroke(group group, hue number, saturation number, lightness number, opacity number|)
sets the outline of each shape in the specified group to the color with the specified hue, saturation, lightness, and sets the outline to the specified opacity.

clone(shape shape|shape)
returns a duplicate of the specified shape, containing duplicates of all the contained contours.

group(elements [group]|group)
returns a new group containing the specified elements.

append(group group, elements [group]|)
appends the specified elements to the specified group.