Before looking at the fractal, we need a simple type for working with
complex numbers that supports the +
operation and the abs
and pow
functions.
We define the type as a simple wrapper over a pair of floating point numbers and
add Abs
and +
as static methods. This way, they can be used through the usual
F# functions:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
|
type Complex =
| Complex of float * float
/// Calculate the absolute value of a complex number
static member Abs(Complex(r, i)) =
let num1, num2 = abs r, abs i
if (num1 > num2) then
let num3 = num2 / num1
num1 * sqrt(1.0 + num3 * num3)
elif num2 = 0.0 then
num1
else
let num4 = num1 / num2
num2 * sqrt(1.0 + num4 * num4)
/// Add real and imaginary components pointwise
static member (+) (Complex(r1, i1), Complex(r2, i2)) =
Complex(r1+r2, i1+i2)
|
Before moving forward, we also need to calculate a power of complex numbers.
To do this, we define a Pow
function in a helper module:
1:
2:
3:
4:
5:
6:
7:
8:
|
module ComplexModule =
/// Calculates nth power of a complex number
let Pow(Complex(r, i), power) =
let num = Complex.Abs(Complex(r, i))
let num2 = atan2 i r
let num3 = power * num2
let num4 = num ** power
Complex(num4 * cos(num3), num4 * sin(num3))
|
Now we have all we need to calculate the Julia set fractal. We choose
a carefuly chosen (handcrafted!) starting point. Then we create a sequence
of powers using F# sequence expressions:
1:
2:
3:
4:
5:
6:
7:
8:
9:
|
/// Constant that generates nice fractal
let c = Complex(-0.70176, -0.3842)
/// Generates sequence for given coordinates
let iterate x y =
let rec loop current = seq {
yield current
yield! loop (ComplexModule.Pow(current, 2.0) + c) }
loop (Complex(x, y))
|
The iterate
lazilly function generates potentially infinite sequence of
values. We take at most max
iterations or stop when the absolute value of
the number is greater than 2. This can be nicely written using Seq
functions
from the standard F# library (supported by Fable):
1:
2:
3:
4:
5:
|
let countIterations max x y =
iterate x y
|> Seq.take (max - 1)
|> Seq.takeWhile (fun v -> Complex.Abs(v) < 2.0)
|> Seq.length
|
To generate a pretty picture, we need to carefuly generate the color palette.
TO do this, we define a pair of operators that let us write
(rgb1) --n--> (rbg2)
and generate a range of colors between rgb1
and rgb2
consisting of n
steps.
1:
2:
3:
4:
5:
6:
7:
8:
|
// Transition between colors in 'count' steps
let (--) clr count = clr, count
let (-->) ((r1,g1,b1), count) (r2,g2,b2) = [
for c in 0 .. count - 1 ->
let k = float c / float count
let mid v1 v2 =
(float v1 + ((float v2) - (float v1)) * k)
(mid r1 r2, mid g1 g2, mid b1 b2) ]
|
Now we can generate palette that is based on Hokusai's famous painting:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
|
// Palette with colors used by Hokusai
let palette =
[| // 3x sky color & transition to light blue
yield! (245,219,184) --3--> (245,219,184)
yield! (245,219,184) --4--> (138,173,179)
// to dark blue and then medium dark blue
yield! (138,173,179) --4--> (2,12,74)
yield! (2,12,74) --4--> (61,102,130)
// to wave color, then light blue & back to wave
yield! (61,102,130) -- 8--> (249,243,221)
yield! (249,243,221) --32--> (138,173,179)
yield! (138,173,179) --32--> (61,102,130) |]
|
The last step is to render the fractal. To do that, we first define a couple of constants
and helpers. The following constants define what part of the fractal we're rendering and
how big is the canvas:
1:
2:
3:
4:
5:
6:
7:
|
// Specifies what range of the set to draw
let w = -0.4, 0.4
let h = -0.95, -0.35
// Create bitmap that matches the size of the canvas
let width = 400.0
let height = 300.0
|
Next, we define setPixel
that sets the RGBA colours of a specified pixel in the canvas
and we'll use F# dynamic operator so that doc?canvas
returns an HTML element with ID
canvas
:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
/// Set pixel value in ImageData to a given color
let setPixel (img:ImageData) x y width (r, g, b) =
let index = (x + y * int width) * 4
img.data.[index+0] <- r
img.data.[index+1] <- g
img.data.[index+2] <- b
img.data.[index+3] <- 255.0
/// Dynamic operator that returns HTML element by ID
let (?) (doc:Document) name :'R =
doc.getElementById(name) :?> 'R
|
The rendering itself is written as an F# asynchronous workflow. The workflow sleeps for
1ms after rendering each line of the fractal. Behind the scenes, this unblocks the window
via a timer, so that the JavaScript function call does not block the browser while running.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
|
/// Render fractal asynchronously with sleep after every line
let render () = async {
// Get <canvas> element & create image for drawing
let canv : HTMLCanvasElement = document?canvas
let ctx = canv.getContext_2d()
let img = ctx.createImageData(U2.Case1 (float width), float height)
// For each pixel, transform to the specified range
// and get color using countInterations and palette
for x in 0 .. int width - 1 do
for y in 0 .. int height - 1 do
let x' = (float x / width * (snd w - fst w)) + fst w
let y' = (float y / height * (snd h - fst h)) + fst h
let it = countIterations palette.Length x' y'
setPixel img x y width palette.[it]
// Insert non-blocking waiting & update the fractal
do! Async.Sleep(1)
ctx.putImageData(img, 0.0, 0.0) }
|
Now we just need to register the event handler for the go
button and start the
asynchronous workflow to do the rendering. Note that this is done using Async.StartImmediate
:
1:
2:
3:
4:
5:
|
/// Setup button event handler to start the rendering
let go : HTMLButtonElement = document?go
go.addEventListener_click(fun _ ->
render() |> Async.StartImmediate; null)
|
namespace Fable
namespace Fable.Core
namespace Fable.Import
module Browser
from Fable.Import
Multiple items
union case Complex.Complex: float * float -> Complex
--------------------
type Complex =
| Complex of float * float
static member Abs : Complex -> float
static member ( + ) : Complex * Complex -> Complex
Full name: Hokusai.Complex
Multiple items
val float : value:'T -> float (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.float
--------------------
type float = System.Double
Full name: Microsoft.FSharp.Core.float
--------------------
type float<'Measure> = float
Full name: Microsoft.FSharp.Core.float<_>
static member Complex.Abs : Complex -> float
Full name: Hokusai.Complex.Abs
Calculate the absolute value of a complex number
val r : float
val i : float
val num1 : float
val num2 : float
val abs : value:'T -> 'T (requires member Abs)
Full name: Microsoft.FSharp.Core.Operators.abs
val num3 : float
val sqrt : value:'T -> 'U (requires member Sqrt)
Full name: Microsoft.FSharp.Core.Operators.sqrt
val num4 : float
val r1 : float
val i1 : float
val r2 : float
val i2 : float
val Pow : Complex * power:float -> Complex
Full name: Hokusai.ComplexModule.Pow
Calculates nth power of a complex number
val power : float
val num : float
static member Complex.Abs : Complex -> float
Calculate the absolute value of a complex number
val atan2 : y:'T1 -> x:'T1 -> 'T2 (requires member Atan2)
Full name: Microsoft.FSharp.Core.Operators.atan2
val cos : value:'T -> 'T (requires member Cos)
Full name: Microsoft.FSharp.Core.Operators.cos
val sin : value:'T -> 'T (requires member Sin)
Full name: Microsoft.FSharp.Core.Operators.sin
val c : Complex
Full name: Hokusai.c
Constant that generates nice fractal
val iterate : x:float -> y:float -> seq<Complex>
Full name: Hokusai.iterate
Generates sequence for given coordinates
val x : float
val y : float
val loop : (Complex -> seq<Complex>)
val current : Complex
Multiple items
val seq : sequence:seq<'T> -> seq<'T>
Full name: Microsoft.FSharp.Core.Operators.seq
--------------------
type seq<'T> = System.Collections.Generic.IEnumerable<'T>
Full name: Microsoft.FSharp.Collections.seq<_>
module ComplexModule
from Hokusai
val countIterations : max:int -> x:float -> y:float -> int
Full name: Hokusai.countIterations
val max : int
module Seq
from Microsoft.FSharp.Collections
val take : count:int -> source:seq<'T> -> seq<'T>
Full name: Microsoft.FSharp.Collections.Seq.take
val takeWhile : predicate:('T -> bool) -> source:seq<'T> -> seq<'T>
Full name: Microsoft.FSharp.Collections.Seq.takeWhile
val v : Complex
val length : source:seq<'T> -> int
Full name: Microsoft.FSharp.Collections.Seq.length
val clr : 'a
val count : 'b
val r1 : int
val g1 : int
val b1 : int
val count : int
val r2 : int
val g2 : int
val b2 : int
val c : int
val k : float
val mid : (int -> int -> float)
val v1 : int
val v2 : int
val palette : (float * float * float) []
Full name: Hokusai.palette
val w : float * float
Full name: Hokusai.w
val h : float * float
Full name: Hokusai.h
val width : float
Full name: Hokusai.width
val height : float
Full name: Hokusai.height
val setPixel : img:ImageData -> x:int -> y:int -> width:float -> r:float * g:float * b:float -> unit
Full name: Hokusai.setPixel
Set pixel value in ImageData to a given color
val img : ImageData
Multiple items
val ImageData : ImageDataType
Full name: Fable.Import.Browser.ImageData
--------------------
type ImageData =
interface
abstract member data : Uint8ClampedArray
abstract member height : float
abstract member width : float
abstract member data : Uint8ClampedArray with set
abstract member height : float with set
abstract member width : float with set
end
Full name: Fable.Import.Browser.ImageData
val x : int
val y : int
val width : float
val g : float
val b : float
val index : int
Multiple items
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
property ImageData.data: Fable.Import.JS.Uint8ClampedArray
val doc : Document
Multiple items
val Document : DocumentType
Full name: Fable.Import.Browser.Document
--------------------
type Document =
interface
inherit DocumentEvent
inherit NodeSelector
inherit GlobalEventHandlers
inherit Node
abstract member addEventListener : type:string * listener:EventListenerOrEventListenerObject * ?useCapture:bool -> unit
abstract member addEventListener_MSContentZoom : listener:Func<UIEvent,obj> * ?useCapture:bool -> unit
abstract member addEventListener_MSGestureChange : listener:Func<MSGestureEvent,obj> * ?useCapture:bool -> unit
abstract member addEventListener_MSGestureDoubleTap : listener:Func<MSGestureEvent,obj> * ?useCapture:bool -> unit
abstract member addEventListener_MSGestureEnd : listener:Func<MSGestureEvent,obj> * ?useCapture:bool -> unit
abstract member addEventListener_MSGestureHold : listener:Func<MSGestureEvent,obj> * ?useCapture:bool -> unit
...
end
Full name: Fable.Import.Browser.Document
val name : string
abstract member Document.getElementById : elementId:string -> HTMLElement
val render : unit -> Async<unit>
Full name: Hokusai.render
Render fractal asynchronously with sleep after every line
val async : AsyncBuilder
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
val canv : HTMLCanvasElement
Multiple items
val HTMLCanvasElement : HTMLCanvasElementType
Full name: Fable.Import.Browser.HTMLCanvasElement
--------------------
type HTMLCanvasElement =
interface
inherit HTMLElement
abstract member getContext : contextId:string * [<ParamArray>] args:obj [] -> U2<CanvasRenderingContext2D,WebGLRenderingContext>
abstract member getContext_2d : unit -> CanvasRenderingContext2D
abstract member ( getContext_experimental-webgl ) : unit -> WebGLRenderingContext
abstract member height : float
abstract member width : float
abstract member msToBlob : unit -> Blob
abstract member height : float with set
abstract member width : float with set
abstract member toBlob : unit -> Blob
...
end
Full name: Fable.Import.Browser.HTMLCanvasElement
val document : Document
Full name: Fable.Import.Browser.document
val ctx : CanvasRenderingContext2D
abstract member HTMLCanvasElement.getContext_2d : unit -> CanvasRenderingContext2D
abstract member CanvasRenderingContext2D.createImageData : imageDataOrSw:U2<float,ImageData> * ?sh:float -> ImageData
type U2<'a,'b> =
| Case1 of 'a
| Case2 of 'b
Full name: Fable.Core.U2<_,_>
union case U2.Case1: 'a -> U2<'a,'b>
val x' : float
val snd : tuple:('T1 * 'T2) -> 'T2
Full name: Microsoft.FSharp.Core.Operators.snd
val fst : tuple:('T1 * 'T2) -> 'T1
Full name: Microsoft.FSharp.Core.Operators.fst
val y' : float
val it : int
property System.Array.Length: int
Multiple items
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken
Full name: Microsoft.FSharp.Control.Async
--------------------
type Async<'T>
Full name: Microsoft.FSharp.Control.Async<_>
static member Async.Sleep : millisecondsDueTime:int -> Async<unit>
abstract member CanvasRenderingContext2D.putImageData : imagedata:ImageData * dx:float * dy:float * ?dirtyX:float * ?dirtyY:float * ?dirtyWidth:float * ?dirtyHeight:float -> unit
val go : HTMLButtonElement
Full name: Hokusai.go
Setup button event handler to start the rendering
Multiple items
val HTMLButtonElement : HTMLButtonElementType
Full name: Fable.Import.Browser.HTMLButtonElement
--------------------
type HTMLButtonElement =
interface
inherit HTMLElement
abstract member checkValidity : unit -> bool
abstract member createTextRange : unit -> TextRange
abstract member autofocus : bool
abstract member disabled : bool
abstract member form : HTMLFormElement
abstract member formAction : string
abstract member formEnctype : string
abstract member formMethod : string
abstract member formNoValidate : string
...
end
Full name: Fable.Import.Browser.HTMLButtonElement
abstract member HTMLElement.addEventListener_click : listener:System.Func<MouseEvent,obj> * ?useCapture:bool -> unit
static member Async.StartImmediate : computation:Async<unit> * ?cancellationToken:System.Threading.CancellationToken -> unit