Make an F# script, say myFirstCanvas.fsx
, with a NuGet reference:
#r "nuget:DIKU.Canvas, 2.0"
open Canvas
open Color
let w,h = 256,256
let tree = filledRectangle green ((float w)/2.0) ((float h)/2.0)
let draw = make tree
render "My first canvas" w h draw
and run it from the command line using
dotnet fsi myFirstCanvas.fsx
|
This should result in a window with a green square in the top left corner on a black background.
If you want a specific version you edit the reference to be, e.g.,:
#r "nuget:DIKU.Canvas, 2.0.5-alpha8"
Make a new directory, say mycanvasapp
, in that directory start an F#
"Console App" project with the command:
dotnet new console -lang "F#"
|
(This will give you both a Program.fs
file and a mycanvasapp.fsproj
file.)
Add a reference to the DIKU.Canvas
package with the command:
dotnet add package DIKU.Canvas
|
Edit Program.fs
to have the content:
open Canvas
open Color
let w,h = 256,256
let tree = filledRectangle green ((float w)/2.0) ((float h)/2.0)
let draw = make tree
render "My first canvas" w h draw
Run your app with the command:
This should result in a window with a green square in the top left corner on a black background.
Graphic primitives may be transformed and combined in a tree structure, and the trees may be rendered to the screen or to file as a still image or an animation. DIKU-Canvas also has an interactive mode that accepts input from the keyboard and the mouse.
The collection of primitives serves as the foundation for complex geometric shapes:
- Piecewise Affine Lines: Represented as sequences of connected line segments, allowing for intricate paths and outlines.
- Circular Arcs: Defined by a center, radius, and angle, enabling precise circular structures.
- Cubic Bezier Curves: Provide control over curve definition and complexity, facilitating the design of smooth and customizable curves.
- Rectangles: Utilize coordinates for position, width, and height to draw various rectangular shapes.
- Ellipses: Create ellipses by specifying parameters that control shape and orientation.
The primitives can be transformed and combined with:
- Translation: Translate objects across the 2D plane with user-defined x and y offsets.
- Rotation: Rotate objects around a specific point, providing the angle in degrees or radians.
- Scaling: Resize objects by a given scaling factor, either uniformly or non-uniformly.
- Horizontal and Vertical Alignment: Utilize alignment functions to organize shapes horizontally or vertically, aiding in layout design.
- Layering Shapes: Combine shapes by drawing them on top of each other, allowing for the creation of intricate designs.
DIKU-Canvas has several rendering and interaction options:
- Render: Graphic trees may be rendered to the screen or a file
- Animation: Sequences of graphic trees may be rendered as an animation to the screen or a file
- Interaction: DIKU-Canvas has an interactive mode, which reacts to the user input from the keyboard or mouse and allows the programmer to update the graphic tree and render the result to the screen.
Canvas is a system for combining simple graphics primitives.
A simple graphics primitive such as a rectangle or an ellipse are leaves in a tree, that is, a single rectangle is a tree with only one node. These can be combined into trees. To illustrate how this works, consider the following figure.
The figure has been produced by rendering the following tree
:
let box1 = rectangle goldenrod 1.0 20.0 80.0
let box2 = rectangle yellow 1.0 30.0 30.0
let tree = alignH (alignV box1 Right box2) Center box1
Here, two boxes are created and combined with the alignV and alignH functions. The resulting tree is depicted below.
Our functional approach allows us to reuse box1 such that it is the same goldenrod rectangle that appears twice. Such reused can be made with any tree.
Canvas can print the tree structure with toString
which gives
AlignH position=0.5
∟>AlignV position=1
∟>Rectangle (color,stroke)=(Color DAA520FF, 1.0) coordinates=(0.0, 0.0, 20.0, 80.0)
∟>Rectangle (color,stroke)=(Color FFFF00FF, 1.0) coordinates=(0.0, 0.0, 30.0, 30.0)
∟>Rectangle (color,stroke)=(Color DAA520FF, 1.0) coordinates=(0.0, 0.0, 20.0, 80.0)
|
indentation illustrates which node is a child of which node. From the output of toString
we cannot see that box1
has been reused. The function toString
also gives other information, e.g., that alignH
was called with the Center
argument which is internally represented as the value 0.5, and likewise, Right
for alignV
is internally represented as the value 1.0.
The value tree
can be rendered on screen by
let pict = make tree
render "Graphics tree" 75 150 pict
which opens a window on the screen showing the 3 boxes. In interactive mode, render
opens a window, and the function returns, when that window is closed. The graphics tree may also be rendered to a file by
let pict = make tree
renderToFile 75 150 "tree.png" tree
In both cases, first tree
is converted to a picture here called pict
, and then a canvas of width 75 and height 150 is created and pict is rendered on it.
Note that the size of the canvas is first specified at the point of rendering, and it is up to the programmer to ensure that the relevant graphics are placed on the canvas. Anything outside the canvas is ignored.
Canvas uses a row-column coordinate system, as illustrated in the figure to the right
which implies that the origin is always rendered in the top left corner of an image and that the first coordinate, x, increases to the right, and the second coordinate, y, increases down. This is a natural coordinate system of images consisting of a table of pixels but may cause confusion when shown on the screen or in a file. For this reason, text
produces images of sequences of letters to be read on the screen, which are intentionally represented upside down, i.e., with the top bar in the letter 'T' being closer to the origin than its foot such that they are shown right way up on the screen.
Each tree has a bounding box, and the bounding box is set differently for each element. For example, the bounding box of a piecewiseAffine
curve is the smallest axis-aligned box, containing the points of the curve, and for a rectangle, it is the same as the rectangle. The bounding boxes are used by the onto
, alignH
, and alignV
alignment commands. alignV
takes two boxes and places the second below the first. It also takes one of Left
, Center
, or Right
values as the alignment parameter, which further aligns the second with the left edge, center, or right edge of the first. alignH
similarly aligns the second bounding box to the right of the first, and its alignment values Top
, Center
, and Bottom
controls the vertical placement of the second box. Like the text
function, the orientation is reversed such that Top
gives an alignment closest to the origin and Bottom
gives an alignment furthest from the origin, to fit the orientation used when rendered on the screen or to a file. The bounding boxes can be rendered for debugging by using the explain
function instead of the make
function.
The workhorse of Canvas is the interact
function. To set up an interactive session, two functions must be defined: draw
and react
. The function react
reacts to input from the user and possibly updates the model, and draw
produces a picture for interact
to render on the screen. The functions draw
and react
communicate through a state value defined by the programmer. The simplest example of this is an interactive session with no state and draw
and react
functions, which ignore their input, as shown below.
let tree = ellipse darkCyan 2.0 85.0 64.0 |> translate 128.0 128.0
let draw _ = make tree
let react _ _ = None
interact "Render an image" 256 256 None draw react 0
When executed, this program opens a window showing an ellipse centered on the screen.
The setup of draw
, interact
, and interact
is essentially how the render
function is implemented.
A more interesting example is shown below, where we define the state value to be a float, which will be used to communicate to draw
where on the screen an ellipse is to be drawn. The react
function is set to react on timer ticks and ignore anything else, and when a timer tick event occurs, it returns a new state value.
type state = float
let tree (i:state) : PrimitiveTree =
ellipse darkCyan 2.0 85.0 64.0 |> translate i i
let draw (j: state) : Picture = make (tree j)
let react (j: state) (ev:Event) : state option =
match ev with
| Event.TimerTick -> Some ((j+1.0)%128.0)
| _ -> None
let interval = Some 100
let initialState = 0.0
interact "Render an image" 256 256 interval draw react initialState
When executed, the window should show an ellipse being translated diagonally down from the top left corner to the bottom right. The speed will be as close to 0.1 seconds per step as possible.
module Canvas
<summary>
Canvas for drawing drawing simple 2D graphics, including features simple user-interaction
</summary>
module Color
<summary>
Utility module for working with colors
</summary>
val w: int
val h: int
val tree: PrimitiveTree
val filledRectangle: c: color -> w: float -> h: float -> PrimitiveTree
<summary>Creates a filled rectangle with the specified width and height.</summary>
<param name="c">The color of the rectangle.</param>
<param name="w">The width of the rectangle.</param>
<param name="h">The height of the rectangle.</param>
<returns>A new graphic primitive tree object representing the filled rectangle.</returns>
<remarks>
The filledRectangle function takes a color, a width, and a height and makes a PrimitiveTree
representing a rectangle, whose lower left corner is (0.0,0.0) and upper right corner is (w,h). Example
of code generating a rectangle tree is:
<code>
let col = goldenrod
filledRectangle col 20.0 80.0
</code>
which generates a PrimitiveTree representing a rectangle filled with the color goldenrod. The bounding
box is the same as the rectangle which in this case is (0.0,0.0) and (20.0,80.0). Note that a
filledRectangle with its outline marked in another color can be achieved by using this function
together with the onto and the rectangle functions.
</remarks>
val green: color
<summary>
The color green
</summary>
Multiple items
val float: value: 'T -> float (requires member op_Explicit)
--------------------
type float = System.Double
--------------------
type float<'Measure> =
float
val draw: Picture
val make: p: PrimitiveTree -> Picture
<summary>Creates a Picture object from a given graphic primitive tree.</summary>
<param name="p">The graphic primitive tree object used to create the graphic primitive tree.</param>
<returns>A new Picture object representing the given graphic primitive tree.</returns>
<remarks>
The functions make and explain both converts a primitive tree to a picture, which can be rendered. For example,
<code>
let tree = ellipse darkCyan 2.0 85.0 64.0 |> translate 128.0 128.0
let picture = make tree
renderToFile 256 256 "sample.tif" picture
</code>
makes a primitive tree consisting of a single ellipse, converts the tree to Picture and renders it to the
file sample.tif as a tif-file.
</remarks>
val render: t: string -> w: int -> h: int -> draw: Picture -> unit
<summary>Runs an application, defining the drawing function.</summary>
<param name="t">The title of the application window.</param>
<param name="w">The width of the application window in pixels.</param>
<param name="h">The height of the application window in pixels.</param>
<param name="draw">A function that returns a Picture object representing the current visual state of the application.</param>
<returns>A unit value indicating the completion of the operation.</returns>
<remarks>
The function render shows a Picture in a window on the screen. For example,
<code>
let tree = ellipse darkCyan 2.0 85.0 64.0 |> translate 128.0 128.0
let pict = make tree
render "Sample title" 256 256 pict
</code>
creates a translated ellipse graphic primitive tree converts it to a Picture using make and shows on the
screen with render.
</remarks>
val box1: PrimitiveTree
val rectangle: c: color -> sw: float -> w: float -> h: float -> PrimitiveTree
<summary>Creates a rectangle with the specified width, height, and stroke width.</summary>
<param name="c">The color of the rectangle.</param>
<param name="sw">The stroke width of the rectangle.</param>
<param name="w">The width of the rectangle.</param>
<param name="h">The height of the rectangle.</param>
<returns>A new graphic primitive tree object representing the rectangle.</returns>
<remarks>
The rectangle function takes a color, a stroke width, a width, and a height and makes a PrimitiveTree
representing a rectangle, whose lower left corner is (0.0,0.0) and upper right corner is (w,h). Example
of code generating a rectangle tree is:
<code>
let col = goldenrod
let strokeWidth = 1.0
rectangle col strokeWidth 20.0 80.0
</code>
which generates a PrimitiveTree representing a rectangle drawn with the color goldenrod and the line
width 1.0. The bounding box is the same as the rectangle which in this case is (0.0,0.0) and (20.0,80.0).
</remarks>
val goldenrod: color
<summary>
The color goldenrod
</summary>
val box2: PrimitiveTree
val yellow: color
<summary>
The color yellow
</summary>
val alignH: pic1: PrimitiveTree -> pos: Position -> pic2: PrimitiveTree -> PrimitiveTree
<summary>Aligns two graphic primitive trees horizontally at a specific position.</summary>
<param name="pic1">The first graphic primitive tree to be aligned.</param>
<param name="pos">One of Top, Center, or Bottom, defining how pic1 and pic2 are to be aligned.</param>
<param name="pic2">The second graphic primitive tree to be aligned.</param>
<returns>A new graphic primitive tree object representing the two graphic primitive trees aligned horizontally at the specified position.</returns>
<remarks>
The alignH function joins two trees where pic2 is translated such that its boundary box's left edge is aligned
with pic1's boundary box's right edge. When pos=Bottom then the boxes are align along their edge with
lowest x-value, i.e., closest to the top edge of the image. When pos=Center then pic2's midpoint in the
y-direction is aligned with pic1's midpoint in the y-direction. When pos=Top then pic2's boundary box's
highest x-value is aligned with pic1's boundary box's highest x-value. The following code-example:
<code>
let box1 = rectangle goldenrod 1.0 20.0 80.0
let box2 = rectangle yellow 1.0 30.0 30.0
alignH box1 Top box2
</code>
represents a new tree of a box from (0,0) to (20,80) in goldenrod and another (20,50) to (50,80)in yellow.
The bounding box is the enclosing box in this case from (0,0) to (50,80).
</remarks>
val alignV: pic1: PrimitiveTree -> pos: Position -> pic2: PrimitiveTree -> PrimitiveTree
<summary>Aligns two graphic primitive trees vertically at a specific position.</summary>
<param name="pic1">The first graphic primitive tree to be aligned.</param>
<param name="pos">One of Left, Center, or Right, defining how pic1 and pic2 are to be aligned.</param>
<param name="pic2">The second graphic primitive tree to be aligned.</param>
<returns>A new graphic primitive tree object representing the two graphic primitive trees aligned vertically at the specified position.</returns>
<remarks>
The alignV function joins two trees where pic2 is translated such that its boundary box's bottom edge is
aligned with pic1's boundary box's top edge. When pos=Left then the boxes are align along their edge with
lowest y-value, i.e., closest to the left edge of the image. When pos=Center then pic2's midpoint in the
x-direction is aligned with pic1's midpoint in the x-direction. When pos=Right then pic2's boundary box's
highest y-value is aligned with pic1's boundary box's highest y-value. The following code-example:
<code>
let box1 = rectangle goldenrod 1.0 20.0 80.0
let box2 = rectangle yellow 1.0 30.0 30.0
alignV box1 Right box2
</code>
represents a new tree of a box from (0,0) to (20,80) in goldenrod and another (-10,80) to (20,100)in yellow.
The bounding box is the enclosing box in this case from (-10,0) to (20,100).
</remarks>
val Right: Position
<summary>An alignv position-value for aligning boxes along their right edge.</summary>
val Center: Position
<summary>An alignh and alignv position-value for aligning boxes along their center.</summary>
val renderToFile: width: int -> height: int -> filePath: string -> draw: Picture -> unit
<summary>Draws a picture generated by make or explain to a file with the specified width and height.</summary>
<param name="width">The width of the output image in pixels.</param>
<param name="height">The height of the output image in pixels.</param>
<param name="filePath">The file path where the image will be saved.</param>
<param name="draw">The picture object to be drawn.</param>
<returns>A unit value indicating the completion of the operation.</returns>
<remarks>
The functions renders a Picture to a file. For example,
<code>
let tree = ellipse darkCyan 2.0 85.0 64.0 |> translate 128.0 128.0
let picture = make tree
renderToFile 256 256 "sample.tif" picture
</code>
makes a primitive tree consisting of a single ellipse, converts the tree to Picture and renders it to the
file sample.tif as a tif-file. The following formats are supported: Bmp, Gif, Jpeg, Pbm, Png, Tiff, Tga, WebP,
and the desired format is specified by the file name suffix. Typically the suffix is given as lower case, and
and Tiff images uses either of the tiff or tif suffixes.
</remarks>
val ellipse: c: color -> sw: float -> rx: float -> ry: float -> PrimitiveTree
<summary>Creates an ellipse with the specified radii and stroke width.</summary>
<param name="c">The color of the ellipse.</param>
<param name="sw">The stroke width of the ellipse.</param>
<param name="rx">The horizontal radius of the ellipse.</param>
<param name="ry">The vertical radius of the ellipse.</param>
<returns>A new graphic primitive tree object representing the ellipse.</returns>
<remarks>
The ellipse function represents an ellipse with center in 0,0, radius rx and ry along the x- and y-axis,
and a color and strokeWidth. Example of code generating an ellipse tree is:
<code>
let col = ivory
let strokeWidth = 3.0
ellipse col strokeWidth 10.0 20.0
</code>
which generates a PrimitiveTree representing an ellipse of radii 10.0 and 20.0 with a line drawn in ivory
and which is 3 wide. The bounding box is the smallest rectangle enclosing the ellipse, which in this case
is (-10,-20) to (10.20).
</remarks>
val darkCyan: color
<summary>
The color darkCyan
</summary>
val translate: dx: float -> dy: float -> p: PrimitiveTree -> PrimitiveTree
<summary>Translates a given graphic primitive tree by the specified distances along the x and y axes.</summary>
<param name="dx">The distance to translate the graphic primitive tree along the x-axis.</param>
<param name="dy">The distance to translate the graphic primitive tree along the y-axis.</param>
<param name="p">The graphic primitive tree to be translated.</param>
<returns>A new graphic primitive tree object that is translated by the specified distances.</returns>
<remarks>
The translate function takes a PrimitiveTree and two floating-point values representing the distances to translate
the tree along the x and y axes, respectively. It constructs a new Translate tree that encapsulates the original tree with a translation primitive.
For example:
<code>
ellipse darkCyan 2.0 85.0 64.0 |> translate 128.0 128.0
</code>
translates the ellipse with radii 85 and 64 and center in 0,0 a such that the resulting ellipse has its center in 128,128.
The ellipse's bounding box is translated accordingly.
</remarks>
union case Option.None: Option<'T>
val interact: t: string -> w: int -> h: int -> interval: int option -> draw: ('s -> Picture) -> react: ('s -> Event -> 's option) -> s: 's -> unit
<summary>Runs an application with a timer, defining the drawing and reaction functions.</summary>
<param name="t">The title of the application window.</param>
<param name="w">The width of the application window in pixels.</param>
<param name="h">The height of the application window in pixels.</param>
<param name="interval">An optional interval for the timer in microseconds.</param>
<param name="draw">A function that takes the current state and returns a Picture object representing the current visual state of the application.</param>
<param name="react">A function that takes the current state and an Event object and returns an optional new state, allowing the application to react to events.</param>
<param name="s">The initial state of the application.</param>
<returns>A unit value indicating the completion of the operation.</returns>
<remarks>
The interact function can render still images in a window, show animations in a window, and
to interact with the user via the keyboard and the mouse. For example,
<code>
let tree = ellipse darkCyan 2.0 85.0 64.0 |> translate 128.0 128.0
let draw _ = make tree
let react _ _ = None
interact "Render an image" 256 256 None draw react 0
</code>
The main workhorses of <c>interact</c> are the draw and react functions, which communicate via a user-defined state
value. In the above example, the state value is implicitly defined as an integer, since the last argument
of interact is of integer type, but both draw and react ignore the state. To make an animation with
interact, the react function must react on TimerTicks. Consider the following example,
<code>
type state = float
let tree (i:state) : PrimitiveTree =
ellipse darkCyan 2.0 85.0 64.0 |> translate i i
let draw (j: state) : Picture = make (tree j)
let react (j: state) (ev:Event) : state option =
match ev with
| Event.TimerTick -> Some ((j+1.0)%128.0)
| _ -> None
let interval = Some 100
let initialState = 0.0
interact "Render an image" 256 256 interval draw react initialState
</code>
Here, we define a state type as float, which is the value controlling what to draw. The draw function
is then a function, which takes a state and produces a Picture in this case the make of an tree containing
an ellipse. The react function is set to listen for TimerTick events. When one such event occurs, it returns
the next value of the state wrapped in an option type (Some). Other events may happen, but they are all
ignored by returning None. Note that there is no mutable value, which contains the present value of the
state. Further, note that the draw function is called inside interact, whenever interact deems it necessary,
such as when react has been called to produce a new value of the state.
The function react is called when the following events occur:
Key of char - when the user presses a regular key
DownArrow - when the user presses the down arrow
UpArrow - when the user presses the up arrow
LeftArrow - when the user presses the left arrow
RightArrow - when the user presses the right arrow
Return - when the user presses the return key
MouseButtonDown(x,y)- when the user presses the left mouse button
MouseButtonUp(x,y) - when the user releases the left mouse button
MouseMotion(x,y,relx,rely) - when the user moves the mouse
TimerTick - when the requested time interval has passed
Note that there is no guarantee that the exact interval has occurred between each TimerTick event, and
depending on the computing system being used, there is a lower limit to how fast an event loop can
be served.
Finally, the state can be any value, and thus the system offers much flexibility in terms of the
communication between the draw and the react function. However, since the programmer (and the user) are only
indirectly in control of their communication, it may be useful to think of draw and react as isolated
functions. E.g., a call by <c>interact</c> to <c>draw j</c> should produce a Picture for state <c>j</c> regardless of the previous
picture or the possible next. Likewise, a call to <c>read j ev</c> should react to the situation specified
by <c>j</c> and <c>ev</c> only, and the programmer should concentrate only on what the next event should be
given said input.
</remarks>
type PrimitiveTree
<summary>A tree of graphics primitives, define as a tree of graphics primitives.</summary>
type Picture
<summary>A picture.</summary>
Multiple items
module Event
from Microsoft.FSharp.Control
--------------------
type Event =
| Key of key: char
| DownArrow
| UpArrow
| LeftArrow
| RightArrow
| Return
| MouseButtonDown of x: int * y: int
| MouseButtonUp of x: int * y: int
| MouseMotion of x: int * y: int * relx: int * rely: int
| TimerTick
<summary>Represents one of the events: Key, DownArrow, UpArrow, LeftArrow, RightArow, Return, TimerTick, MouseButtonDown, MouseButtonUp, and MouseMotion.</summary>
--------------------
type Event<'T> =
new: unit -> Event<'T>
member Trigger: arg: 'T -> unit
member Publish: IEvent<'T>
--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate and reference type)> =
new: unit -> Event<'Delegate,'Args>
member Trigger: sender: obj * args: 'Args -> unit
member Publish: IEvent<'Delegate,'Args>
--------------------
new: unit -> Event<'T>
--------------------
new: unit -> Event<'Delegate,'Args>
type 'T option = Option<'T>
union case Event.TimerTick: Event
<summary>
A tick event from the timer
</summary>
union case Option.Some: Value: 'T -> Option<'T>