views:

99

answers:

3

Hi all,

I have the following codes to draw an unit circle

open System
open Microsoft.FSharp.Collections
open Microsoft.FSharp.Math
open System.Drawing
open System.Windows.Forms

let make_point (x:float) (y:float) = (fun bit -> if bit = 0.0 then x else y)
let x_of (point:float->float) = point 0.0
let y_of (point:float->float) = point 1.0

let unit_circle (t:float) = 
    make_point (sin <| 2.0 * Math.PI * t)
               (cos <| 2.0 * Math.PI * t)
let draw_connected (curve:float->float->float) (values: float list)=
    let form = new Form(Text = "Curve")
    let drawCurve (g:Graphics) = 
        for t in values do
            let p = curve t        
            g.DrawEllipse(Pens.Red, 
                          float32 (x_of p * 50.0 + (float)form.ClientSize.Width / 2.0), 
                          float32 (y_of p * 50.0 + (float)form.ClientSize.Height / 2.0), 
                          float32 1, 
                          float32 1)
    form.Paint.Add(fun e -> drawCurve e.Graphics)    
    form.Show()

draw_connected unit_circle ([0.0 .. 0.01 .. 1.0])

I am not entirely satisfied because I have to manual "scale" the x and y coordinates by 50 to make the circle visible. Is there a way to get F# do the scaling automatically?

Thanks.

+1  A: 

I didn't fully try to understand your code, but perhaps you could use the scale transformation that can be specified to the Graphics object. This changes the coordinate system of Graphics, so all drawing that you perform (e.g. using DrawEllipse) is automatically scaled - you could set scaling in a way such that unit circle appears as circle with radius 50.

  • To set the transformation, use the ScaleTransfrom method (see MSDN documentation for more information) of Graphics instance (the value g in your code).
Tomas Petricek
+1  A: 

I think the code is representing a 2D point as a function taking 3 args - a flag, x & y. The flag indicates which of x and y to return. It would make (slightly) more sense for a start if the flag was a bool rather than a float. I'm guessing the code has been converted from another language which only has floats?

Here's a slightly more comprehensible version:

open System
open Microsoft.FSharp.Collections
open Microsoft.FSharp.Math
open System.Drawing
open System.Windows.Forms
open System.Threading

type Point = {x : float; y : float}

let unit_circle (angle : float) = 
    {
        x = (sin <| 2.0 * Math.PI * angle)
        y = (cos <| 2.0 * Math.PI * angle)
    }

let draw_connected (curve : float -> Point) (radius : float) (angles : float list) =
    let form = new Form(Text = "Curve")
    let drawCurve (gfx : Graphics) =
        for angle in angles do
            let p = curve angle        
            gfx.DrawEllipse(Pens.Red, 
                          float32 (p.x * radius + (float)form.ClientSize.Width / 2.0), 
                          float32 (p.y * radius + (float)form.ClientSize.Height / 2.0), 
                          float32 1,
                          float32 1)
    form.Paint.Add (fun pntEvntArgs -> drawCurve pntEvntArgs.Graphics)    
    form.Show ()
    form

let form = draw_connected unit_circle 50.0 ([0.0 .. 0.01 .. 1.0])

while form.Created do
    Thread.Sleep (1)
    Application.DoEvents ()
done

Not sure why the circle is rendered as a collection of 1 pixel ellipses.

In any case, as Tomas says, either the circle has to be scaled or the coordinate system does. Otherwise you'll end up with a 1 pixel circle.

jon hanson
A: 

As Tomas said you can use scaling transformations. If you want to draw a circle using small curves you can use multiple DrawCurve calls :)

I've changed a bit jon's code for that purpose:

  • used System.Drawing.Point type instead of your record
  • modified unit_circle so that it returns a tuple representing coordinates x and y
  • turned your list of angles into a sequence of sequence of angles. This will be useful since we can have a variable number of knots for our curve (a cardinal spline), represented by the constant N
  • implemented a splitRepeatEvery method, e.g:

    Seq.splitRepeatEvery 3 { 1 .. 10 } returns seq [seq [1; 2; 3]; seq [3; 4; 5]; seq [5; 6; 7]; seq [7; 8; 9]; seq [9; 10]]

Here's the code:

module Seq =
    /// Split a sequence into pieces, each n items long
    /// repeating elements between start and end of subsequences.
    let splitRepeatEvery (count : int) (source : seq<'T>) = 
        if not (count > 1) then failwith "count must be superior to 1"
        seq { use e = source.GetEnumerator()
              let hasNext = ref (e.MoveNext())
              while !hasNext do
                 let (block:option<'T>[]) = Array.create count None

                 for i in 0 .. count - 1 do
                     do block.[i] <- if !hasNext then Some(e.Current) else None
                     if (i <> count - 1) then do hasNext := e.MoveNext()

                 yield seq { yield! block }
                 |> Seq.filter (fun x -> x.IsSome)
                 |> Seq.map (function Some(e) -> e | _ -> failwith "" ) }

let unit_circle (angle : float) = 
    (sin <| 2.0 * Math.PI * angle), (cos <| 2.0 * Math.PI * angle)

let draw_connected curve radius (seqOfAngles : float seq seq) knotsCount =
    let form = new Form(Text = "Curve")

    let computeBoundingBox points =
        let search f acc array =
            Array.fold (fun (x,y) (p:Point) -> f p.X x, f p.Y y) acc array
        let minX, minY = search min (form.ClientSize.Width, form.ClientSize.Height) points
        let maxX, maxY = search max (0,0) points
        new Rectangle(minX, minY, abs(minX-maxX), abs(minY-maxY))

    let drawCurves (gfx : Graphics) =
        // Create a buffer for storing our knots
        let buffer = Array.create knotsCount (new Point())
        let mutable i = 0

        for angles in seqOfAngles do
            for angle in angles do
                let x, y = curve angle
                let X = int(x * radius + (float)form.ClientSize.Width  / 2.0)
                let Y = int(y * radius + (float)form.ClientSize.Height / 2.0)
                let P = new Point(X, Y)
                buffer.[i] <- P
                i <- i + 1

            let knots = buffer.[0..(i-1)]
            // Draw spline only if we have one or more knots
            if knots.Length <> 1 then
                gfx.DrawCurve(Pens.Red, knots)
                // For debug: compute BBox of an array of points and draw it
                let debugRect = computeBoundingBox knots
                gfx.DrawRectangle(Pens.Black, debugRect)

            // Don't forget to reset position in buffer between each spline draw call
            i <- 0

    form.Paint.Add (fun pntEvntArgs -> drawCurves pntEvntArgs.Graphics)    
    form.Show ()
    form

// Define constants    
let STEP = 0.050
let N = 4
// Define a new sequence of sequence of angles
let s = {0.0 .. STEP .. 1.0} |> Seq.splitRepeatEvery N
let form = draw_connected unit_circle 120.0 s N

// For debug: print sequence of sequence of angles
s |> Seq.iter (fun s -> Seq.iter (fun x -> printf "%f " x) s; printfn "")

while form.Created do
    Thread.Sleep (1)
    Application.DoEvents ()
done

You can play with different values of N (number of knots for the splines) and STEP (but beware STEP should be choosen so that 1.0f is a multiple of STEP or a floating point number so that last element of last sequence is close enough to 1.0f else the last spline won't connect to the first one!). And voila!

alt text

Stringer Bell