I am new to Haskell, and have been reading Learn You a Haskell for Great Good!.
I've rewritten a problem that I recently solved in JavaScript in Haskell to practice what I have been reading.
A little bit of a back story with out rambling on too much; I wanted to create a circle using bezier curve drawing commands. Similar to SVG but for an Android vector drawable. The reason I needed the circle in a path command is for animation. I was using AnimatedVectorDrawable to animate a circle into a square. If your drawable has the same path with the same number of commands in two different files it can animate fluidly between them. Making a square with curve commands is easy enough, but the circle needed some math.
What I did to quickly generate my circle path was create a script in Javascript to do it. Being a math problem I felt this was a good candidate for Haskell exercise.
Here is my Haskell script:
-- Usage:
-- beziercircle [circumfrance] [offest x] [offset y]
--
-- Example:
-- beziercircle 500
--
--
-- Calculates a circle using bezier paths for using in android
-- vector drawables
--
import System.Environment
import Text.Printf
bz = 0.552284749831
zero = read "0" :: Float
main = do
args <- getArgs
let c = parseArg 0 args
let d = c / 2
let ox = parseArg 1 args
let oy = parseArg 2 args
let ps = points d ox oy
let cs = controls zero (ip d) (op d) c ox oy
putStrLn $ (showMove (zero + ox, d + oy)) ++ (showAllCurves ps cs) ++ "Z"
where
parseArg i args = (if length args >= i + 1 then read (args !! i) :: Float else zero)
ip d = d - (d * bz)
op d = d + (d * bz)
controls a b c d x y = map (\(a, b, c, d) -> (ox a,oy b,ox c,oy d)) [c1, c2, c3, c4]
where ox = (+x)
oy = (+y)
c1 = (a, b, b, a)
c2 = (c, a, d, b)
c3 = (d, c, c, d)
c4 = (b, d, a, c)
points d x y = map (\(x,y) -> (ox x, oy y)) [p2, p3, p4, p1]
where ox = (+x)
oy = (+y)
p1 = (zero, d)
p2 = rotate90 p1
p3 = offset d p2
p4 = offset d p1
offset o (x, y) = (x + o, y + o)
rotate90 (x, y) = (y, x * (-1))
showMove (x, y) = printf "M %f %f \n" x y
showCurve (x, y) (cx1, cy1, cx2, cy2) = do
printf "C %f %f %f %f %f %f \n" cx1 cy1 cx2 cy2 x y
showAllCurves as bs = concat $ zipWith (showCurve) as bs
The script is based on this question How to create circle with Bezier Curves
Being new I am sure there are lots of places I can make this script better. I am hoping to learn from feedback! I know it can be commented more, mostly looking for ways to shrink the code using techniques that may not be familiar to me. Even if there is a Math equation that I am no seeing, that can solve this better. That would be great to learn as well!
Here is my original Javascript (Node.js) for reference as well:
// Creates a Vector path command for a circle based on based in circumfrence
var args = process.argv.slice(2);
if (args.length < 1) {
console.log("Please supply a width for the vector circle path");
}
// The second and third argument can be used to offset the circle by X adn Y number of pixels
var offsetXBy = 0;
var offsetYBy = 0;
if (args.length >= 2) {
offsetXBy = parseFloat(args[1]);
}
if (args.length >= 3) {
offsetYBy = parseFloat(args[2]);
}
function Coord(x, y) {
var self = this;
this.x = parseFloat(x);
this.y = parseFloat(y);
this.offset = function(offsetX, offsetY) {
self.x += offsetX;
self.y += offsetY;
}
}
Coord.prototype.toString = function () {
return `${this.x} ${this.y}`
}
function BezierCurve(x, y, c1, c2) {
var self = this;
this.x = x;
this.y = y;
this.c1 = c1;
this.c2 = c2;
this.offset = function(offsetX, offsetY) {
self.x += offsetX;
self.y += offsetY;
self.c1.offset(offsetX, offsetY);
self.c2.offset(offsetX, offsetY);
}
}
BezierCurve.prototype.toString = function () {
return `${this.c1.toString()} ${this.c2.toString()} ${this.x} ${this.y}`
}
var BEZIER_CONTROL_POINT = 0.552284749831;
var dimension = parseFloat(args[0]); // Circles circumfrence
var halfDimen = dimension / 2;
var controlPointOffset = halfDimen * BEZIER_CONTROL_POINT;
// from Middle Left
var firstMove = new Coord(0, halfDimen);
// curve to Top Middle
var curve1 = new BezierCurve(halfDimen, 0,
new Coord(0, halfDimen - controlPointOffset),
new Coord(halfDimen - controlPointOffset, 0));
// curve to Middle Right
var curve2 = new BezierCurve(dimension, halfDimen,
new Coord(halfDimen + controlPointOffset, 0),
new Coord(dimension, halfDimen - controlPointOffset));
// curve to Bottom Middle
var curve3 = new BezierCurve(halfDimen, dimension,
new Coord(dimension, halfDimen + controlPointOffset),
new Coord(halfDimen + controlPointOffset, dimension));
// curve back to Middle Left
var curve4 = new BezierCurve(0, halfDimen,
new Coord(halfDimen - controlPointOffset, dimension),
new Coord(0, halfDimen + controlPointOffset));
if (offsetXBy > 0) {
firstMove.offset(offsetXBy, offsetYBy);
curve1.offset(offsetXBy, offsetYBy);
curve2.offset(offsetXBy, offsetYBy);
curve3.offset(offsetXBy, offsetYBy);
curve4.offset(offsetXBy, offsetYBy);
}
console.log(`M ${firstMove.toString()}`);
console.log(`C ${curve1.toString()}`);
console.log(`C ${curve2.toString()}`);
console.log(`C ${curve3.toString()}`);
console.log(`C ${curve4.toString()}`);
console.log("Z");