Hokusai and fractals

Rendering fractals using HTML5 canvas

This demo is based on Tomas Petricek's F# Advent Calendar post that explores Japanese art using the (now defunct) Freebase type provider and renders The Great Wave by Hokusai using the Julia fractal. You can find the full source code on GitHub.

In this demo, you'll see how to define a simple complex number arithmetic in F#, how to use it to implement the Julia set fractal and how to render the fractal asynchronously to avoid blocking the browser during the process. To run the demo, click the "Render Julia set fractal" button!


Complex numbers

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))

Calculating the Julia set

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

Generating the color palette

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) |]

Drawing the fractal

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 Choice : computations:seq<Async<'T option>> -> Async<'T option>
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
Fork me on GitHub