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.
.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.
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))
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))
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 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))
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))
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))
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 combine two expressions and produce a single value. The following operators are defined:
+
Add - Combine two numbers into their sum-
Subtract - Combine two numbers into their difference*
Multiply - Combine two numbers into their product/
Divide - Combine two numbers into their quotient>
Greater - Combine two numbers into a boolean indicating whether the first number is
larger than the second<
Less - Combine two numbers into a boolean indicating whether the first number is
smaller than the second==
Equal - Combine two numbers into a boolean indicating whether the first number is equal
to the second!=
Differ - Combine two numbers into a boolean indicating whether the first number differs
from the second
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))
The number type represents a 64-bit double-precision floating point value (IEEE754). Number literals must begin with a digit.
Prefix operators convert the value from a single following expression into a different value. The following prefix operators are defined:
!
Not - Converts the following boolean value into the opposite boolean value-
Negate - Converts the following number value into the negation of the value
height := 20
condition := false
width := -20
if !condition {
width = -width
}
shape := rectangle(width, height)
return(translate(shape, 100, 100))
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))
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))
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.
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 contours
s.
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
.