canvas


Getting started

How to use Canvas in an F# script (.fsx)

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.3-alpha8"

How to use Canvas in a F# project (that uses .fsproj)

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:

dotnet run

This should result in a window with a green square in the top left corner on a black background.

What is Canvas

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.

Primitives

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.

Transformations and composition

The primitives can be transformed and combined with:

Rendering and interaction with the user

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.

Overview

Canvas is a system for combining simple graphics primitives.

The graphics tree structure

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.

Rendering on a canvas

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.

Making animations and interacting with the user

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 |&gt; 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 |&gt; 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 |&gt; 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 |&gt; 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 |&gt; 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 |&gt; translate i i let draw (j: state) : Picture = make (tree j) let react (j: state) (ev:Event) : state option = match ev with | Event.TimerTick -&gt; Some ((j+1.0)%128.0) | _ -&gt; 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>