Custom Pointers Coding Toolkit

There's a little known and (relatively) un-Documented (until now) feature of Fantasy Grounds that allows us to create and use our own Custom Pointers inside ImageControls. Getting it right is not easy but I've created a Toolkit that should make things a whole lot easier - and here it is. Read it all (if you want to know how to do things the Toolkit doesn't cover) and have fun.

Note that the LUA code in the Toolkit is not meant to be used "as is" - you will need to modify some of it to fit your particular requirements.

Background/Theory

A Pointer in Fantasy Grounds is the Circle, Square, Cone and Arrow shapes we draw on our Images when we are gaming - for Spell Effect areas, etc.

The Default Pointers included with Fantasy Grounds are a series of Splines. Originally a Spline was a flexible piece of metal or wood shaped and fixed at certain points along its length and then allowed to form a natural series of curves. The resulting curves had the least amount of stress on them and flowed naturally from one into the other. These curves were then copied down onto graph paper and transfer to make various physical items, such as boat hulls and aircraft wings.

Since the age of Computer Aided Design (CAD) the term Spline has come to mean a series of smooth curves defined by Mathematical Equations. The Mathematical Equations have different orders of magnitude, and consist of Linear Equations, Quadratic Equations, Cubic Equations, Quartic Equations, Quintic Equations, Sextic Equations, etc.

"Oh God" I hear you cry, "Do I have to learn all this Algebra and Polynomial Mathematics (again)?"

No, and here's why:

The Curves that make up the Splines that make up the Pointers in Fantasy Grounds are known as B-Curves - and are in fact a particular type of B-Curve known as a Bézier Curve. In particular, they are Cubic Bézier Curves.

A Cubic Bézier Curve is defined by four points: two End Points and two Control Points. The Curve leaves an End Point and heads towards (but normally never touches) the nearest Control Point before curving away to meet the part of the curve coming from the other End Point, which itself began its journey heading towards its own Control Point. Depending upon how close a Control Point is to its End Point and compared to the distance of the other End Point/Control Point pair determines the amount of curvature of a given length of the Curve. If both Control Points are the same distance from their respective End Points then the resulting Curve will be regular (ie symmetrical around the Mid-Point of the Curve), otherwise we end up with an irregular Curve.

By its definition, for two such Curves to form a Spline they must meet and flow from one to the other smoothly. To do this not only must the End Point of one Curve be the End Point of another (ie have the same coordinates), but the End Point(s) and their respective Control Points MUST form a straight line; if they do not, then the resulting two Curves DO NOT form a Spline.

In Fantasy Grounds the four points that make up a Cubic Bézier Curve are represented as an array (table) of four rows and two columns in the form of:

Code:

aCurve = {

{ nStartPointXCoord, nStartPointYCoord },
{ nStartPointControlXCoord, nStartPointControlYCoord },
{ nEndPointControlXCoord, nEndPointControlYCoord },
{ nEndPointXCoord, nnEndPointYCoord }

};

The entire shape to be drawn is represented by an array of these aCurves and is built as follows:

Code:

table.insert(aShape,aCurve1);

table.insert(aShape,aCurve2);

[... etc ...]

Usage

To access the Custom Pointer functionality we need to add the following code to our ImageControl. The "standard" ImageControl for the CoreRPG is located in the campaign_images.xml file in the Campaign Folder (once the CoreRPG pak file has been unpacked - it starts at line 28):

Code:

<imagecontrol>

<pointertypes>
<custom name="45Cone">
<icon>pointer_cone</icon>
<label>Draw a 45-Degree Cone</label>
</custom>
</pointertypes>
[... Other Lines Of Code ...]

</imagecontrol>

Up to five PointerTypes can be defined. The order that they are defined is the order that the Buttons will appear on the relevant RadialMenu.

By default, the four Default Pointers which come with Fantasy Grounds are defined as:

Code:

<imagecontrol>

<pointertypes>
<arrow>
<icon>pointer</icon>
<label>Draw Arrow</label>
</arrow>
<square>
<icon>pointer_square</icon>
<label>Draw Square</label>
</square>
<circle>
<icon>pointer_circle</icon>
<label>Draw Circle</label>
</circle>
<cone>
<icon>pointer_cone</icon>
<label>Draw Cone</label>
</cone>
</pointertypes>
[... Other Lines Of Code ...]

</imagecontrol>};

If these are the only Pointers being used they do not need to be defined (they are defined by default). However, if you wish to use only some of these Default Pointers (with or without one or more Custom Pointers) or any or all of these Default Pointers in addition to any custom Pointers then their individual definitions need to be included in your ImageControl. Default Pointers not included will NOT appear on the RadialMenu.

The four Default Pointers are always available via the Left-And-Right Mouse Button technique.

Once you define a Custom Pointer the function onBuildCustomPointer( nStartXCoord, nStartYCoord, nEndXCoord, nEndYCoord, sPointerType ) becomes avalible. The five parameters are the Starting X-Coordinate, the Starting Y-Coordinate, the Ending X-Coordinate, the Ending Y-Coordinate (of the Custom Pointer as a whole - all numerics) and the Custom Pointer Type (a string that matches the "name" attribute of the Custom tag of the Pointertypes tag - see above). The function returns aShapeCurves (an array of Curves as defined above), aDistLabelPosition (an array of two numbers that specify where the Pointer Length Label should be drawn relative to its origin) and bDrawArrow (a boolean (ie "true" of "false") indicating whether to end the Pointer with an Arrowhead pointing in the direction of the last Curve).

"OK, so how do I know where to place my Control Points to get the curve I want?" you ask.

Well, you could just enter in the coordinates for the four points and see how the curve looks, then go back and change them to try again, or you could actually learn the Mathematics involved and work it out with a calculator, but instead I've written an LUA file with all of the code you should need in it (shown in its entirety below).

To use the file add it to the ImageContol:

Code:

<imagecontrol>

<script file="Pointer_Toolkit.lua" />
[... Other Lines Of Code ...]

</imagecontrol>};

When working with the code I realised that, thanks to the pioneering work of Tero Parvinen (one of the Fantasy Grounds Coders) the easiest way to draw the Pointers was to drawn then in the Negative Y-Direction (ie down the image) with an Origin (or Starting Point) of (0,0) and symmetrical around the Y-Axis, then rotating the Curves and transposing them to their correct orientation and position. When using the Toolkit this is how you should construct your Pointers.

I also determined that, fundamentally, normally all of the Pointers we would probably want can be drawn with but three basic Curves: a Line (a straight Curve), the quarter arc of an Ellipse (a quarter of an Oval) and the Arc of a Circle.

The code for a Line relies on the fact that a Line's Control Points are the Line's End Points. The Offset value (see below) is used to move the entire Line up or down the Y-Axis and is normally 0 (see below for an exception):

Code:

function fpLineCurve(nStartLineXCoord,nStartLineYCoord,nEndLineXCoord,nEndLineYCoord,nOffset)

-- Draw a Line offset in the Negative-Y direction by nOffset.
local aCurve = {{nStartLineXCoord,nStartLineYCoord-nOffset},
{nStartLineXCoord,nStartLineYCoord-nOffset},
{nEndLineXCoord,nEndLineYCoord-nOffset},
{nEndLineXCoord,nEndLineYCoord-nOffset}};
return aCurve;

end};

The code for an Ellipse Curve draws a quarter Ellipse in only one quadrant of the X-Y Plane (which quadrant depends upon the sign on the two Radii that help define the Ellipse Curve). An Ellipse has two Radius values. The relative size of the two Radii determine the narrowness of the resulting Ellipse. If the two Radii are the same the resulting Curve is a quarter Circle. The value of Kappa is a constant and is ONLY valid for quarter Ellipse/Circle Curves. If you try to use it for Curves less than or greater than 90 Degree Curves the resulting Curve will NOT be an accurate Ellipse/Circle. The Offset is used to move the entire Ellipse Quarter up or down the Y-Axis and is normally 0 (see below for an exception):

Code:

-- Draw a 90-Degree Arc of an Ellipse offset in the Negative-Y direction by nOffset.
local nKappa = 4/3x(math.sqrt(2)-1);
local aCurve = {{0,nYRadius-nOffset},
return aCurve;

end};

Writing a generic Circle Curve function that could drawn an Arc of any Radius anywhere at any Rotation proved difficult and instead of one I ended up with two functions which both call a third to do the work.

The first (fpAngleArcCurve()) is used when you know the Starting Point of the Curve, its Radius/Centre and the Angle it should cover, and takes as parameters the Starting X and Y Coordinates of the Curve, the X and Y Coordinates of the Centrepoint of the Circle the Curve is a part of and the Angle that the Curve needs to cover (nArcDegrees) in Degrees. nArcDegrees must be between -180 and +180 (not inclusive) and must not be 0.

The second (fpEndpointArcCurve()) is used when you know the Starting and Ending Points of the Curve and its Radius/Centre, and takes as parameters the Starting X and Y Coordinates of the Curve, the X and Y Coordinates of the Centrepoint of the Circle the Curve is a part of and the Ending X and Y Coordinates of the Curve.

Both functions call the third function (fpArcCurve()) after first manipulating values. fpArcCurve() draws an Arc of the desired Radius in the Positive X-Direction and bisected by the X-Axis. It then rotates and transposes the Arc to the desired position and rotation as defined by the calling function.

Code:

function fpAngleArcCurve(nStartCurveXCoord,nStartCurveYCoord,nCurveCentreXCoord,nCurveCentreYCoord,nArcDegrees)

-- Draw a Circular Arc covering nArcDegreess (-180 < nArcDegrees < 180, nArcDegrees ~= 0) given
--        the Circle Centre and the Arc Start Point.
if nArcDegrees == 0 or
nArcDegrees <= -180 or
nArcDegrees >= 180 then
return;
end
local nRadius = math.sqrt((nCurveCentreXCoord-nStartCurveXCoord)^2+(nCurveCentreYCoord-nStartCurveYCoord)^2);
if nRadius == 0 then
return;
end
end

function fpEndpointArcCurve(nStartCurveXCoord,nStartCurveYCoord,nCurveCentreXCoord,nCurveCentreYCoord,nEndCurveXCoord,nEndCurveYCoord)
-- Draw a Circular Arc given the Circle Centre, the Arc Start Point and the Arc End Point.
local nStartAngleRadians = math.atan2(nStartCurveXCoord-nCurveCentreXCoord,nCurveCentreYCoord-nStartCurveYCoord);
local nEndAngleRadians = math.atan2(nEndCurveXCoord-nCurveCentreXCoord,nCurveCentreYCoord-nEndCurveYCoord);
if nArcRadians == 0 or
return;
end
local nRadius = math.sqrt((nCurveCentreXCoord-nStartCurveXCoord)^2+(nCurveCentreYCoord-nStartCurveYCoord)^2);
if nRadius == 0 then
return;
end
end

-- Draw an Regular Arc (of a Circle) of Radius nRadius with an Origin of (0,0) and covering
--        an Arc of nArcRadians (in Radians) bisected by the Positive X-Axis and then Rotated
--        around the Origin by an angle of nAngleRadians (in Radians) and offset in both the
--        X-Direction and Y-Direction by nCurveCentreXCoord and nCurveCentreYCoord respectively.
local nX = math.cos(nArcRadians/2);
local nY = math.sin(nArcRadians/2);
local nStartX = nXxnRadius;
local nStartY = nYxnRadius;
local nControlX = nRadiusx(4-nX)/3;
local nControlY = nRadiusx(1-nX)x(3-nX)/(3xnY);
local aCurve = {{nStartX,nStartY},
{nControlX,nControlY},
{nControlX,-nControlY},
{nStartX,-nStartY}};
for nPointIndex,aPoint in ipairs(aCurve) do
aCurve[nPointIndex] = {nXCoord,nYCoord};
end
return aCurve;

end};

So, how do you use these Curve Definition Functions? Well, I've included some sample generic Pointer Definition Functions to show you how.

The first (fpBoxPointer()) draws a Box with a given Length and Width and is made up of four Lines. If the Length and Width are the same then we get a replica of the Default Square Pointer. The Offset is used to move the entire Box up or down the Y-Axis and is normally 0 (see below for an exception):

Code:

function fpBoxPointer(aShapeCurves,nLength,nWidth,nOffset)

-- Draw a Rectangle offset in the Negative-Y direction by nOffset.
table.insert(aShapeCurves,fpLineCurve(nWidth,nLength,-nWidth,nLength,nOffset));
table.insert(aShapeCurves,fpLineCurve(-nWidth,nLength,-nWidth,-nLength,nOffset));
table.insert(aShapeCurves,fpLineCurve(-nWidth,-nLength,nWidth,-nLength,nOffset));
table.insert(aShapeCurves,fpLineCurve(nWidth,-nLength,nWidth,nLength,nOffset));

end};

The second (fpCirclePointer()) draws a replica of the default Circle Pointer with a given Radius and is made up of eight equal Arcs.

Code:

-- Draw a Circle of Radius nRadius made up of eight Regular Arcs.

end};

The third (fpConePointer()) draws a Cone of the given Radius and covering the given Angle and is made up of two Lines and an Arc. If nArcDegrees is set to 90 we get a replica of the Default Cone Pointer.

Code:

-- Draw a Cone with a Radius of nRadius and covering an Arc of nArcDegrees.
if nArcDegrees == 0 or
nArcDegrees <= -180 or
nArcDegrees >= 180 then
return;
end
table.insert(aShapeCurves,fpLineCurve(0,0,nXCoord,-nYCoord,0));
table.insert(aShapeCurves,fpLineCurve(0,0,-nXCoord,-nYCoord,0));

end};

Finally, the forth function (fpEllipsePointer()) draws an Ellipse of the given Radii and is made up of four Ellipse Curves (one in each quadrant). The Offset is used to move the entire Ellipse up or down the Y-Axis and is normally 0 (see below for an exception):

Code:

-- Draw an Ellipse offset in the Negative-Y direction by nOffset.

end};

Examples

Finally, I've included a sample onBuildCustomPointer() function with eleven sample Custom Pointers to demonstrate some ways to use each of the Curve Definition Functions. Each can be used as is by setting the "name" attribute of the Custom tag in the ImageControl to the given name shown. The onBuildCustomPointer() function also does the final rotation and transposition of the given Pointer.

The first (CirclePointerAsEllipse) uses the fpEllipsePointer() function to replicate the Default Circle Pointer.

The second (CirclePointerAsArcs) uses the fpCirclePointer() function to replicate the Default Circle Pointer.

The third (HalfWidthEllipsePointerCenterOrigin) uses the fpEllipsePointer() function to draw an Ellipse which is half as wide as it is long.

The fourth (HalfWidthEllipsePointerStartPointOrigin) uses the fpEllipsePointer() function to draw an Ellipse which is half as wide as it is long and which starts at the Starting Point instead of the Starting Point being in the centre of the Pointer. I use this Pointer in my Übergame Ruleset to represent the Breath Weapon of a Green Dragon (a cloud of noxious chlorine gas).

The fifth (SquarePointer) uses the fpBoxPointer() function to replicate the Default Square Pointer.

The sixth (DoubleWidthBoxPointerCenterOrigin) uses the fpBoxPointer() function to draw a Rectangle which is twice as wide as it is long.

The seventh (DWBoxPointerStartPointOrigin) uses the fpBoxPointer() function to draw a Rectangle which is twice as wide as it is long and which starts at the Starting Point instead of the Starting Point being in the centre of the Pointer.

The eighth (ConePointer) uses the fpConePointer() function to replicate the Default Cone Pointer.

The ninth (60ConePointer) uses the fpConePointer() function to draw a 60 Degree Cone. I use this Pointer in my Übergame Ruleset to represent the Cone function of all Spells, etc.

The tenth (120ConePointer) uses the fpConePointer() function to draw a 120 Degree Cone. I use this Pointer in my Übergame Ruleset to represent the Burning Hands Spell.

Finally, ArrowPointer uses the fpLineCurve() function along with setting bDrawArrow to true to replicate the Default Arrow Pointer.

Code:

function onBuildCustomPointer(nStartXCoord,nStartYCoord,nEndXCoord,nEndYCoord,sPointerType)

local nLength = math.sqrt((nEndXCoord-nStartXCoord)^2+(nEndYCoord-nStartYCoord)^2);
if nLength == 0 then
return
end
local aShapeCurves = {};
local aDistLabelPosition = {25,25};
local bDrawArrow = false;
local nAngleRadians = math.atan2(nEndXCoord-nStartXCoord,nStartYCoord-nEndYCoord);
-- Call the relevant Pointer Definition Function
-- Sample PointerTypes Shown
if sPointerType == "CirclePointerAsEllipse" then
fpEllipsePointer(aShapeCurves,nLength,nLength,0);
elseif sPointerType == "CirclePointerAsArcs" then
fpCirclePointer(aShapeCurves,nLength);
elseif sPointerType == "HalfWidthEllipsePointerCenterOrigin" then
fpEllipsePointer(aShapeCurves,nLength/2,nLength,0);
elseif sPointerType == "HalfWidthEllipsePointerStartPointOrigin" then
fpEllipsePointer(aShapeCurves,nLength/4,nLength/2,nLength/2);
elseif sPointerType == "SquarePointer" then
fpBoxPointer(aShapeCurves,nLength,nLength,0);
elseif sPointerType == "DoubleWidthBoxPointerCenterOrigin" then
fpBoxPointer(aShapeCurves,nLength,nLengthx2,0);
elseif sPointerType == "DWBoxPointerStartPointOrigin" then
fpBoxPointer(aShapeCurves,nLength/2,nLength,nLength/2);
elseif sPointerType == "ConePointer" then
fpConePointer(aShapeCurves,nLength,90);
elseif sPointerType == "60ConePointer" then
fpConePointer(aShapeCurves,nLength,60);
elseif sPointerType == "120ConePointer" then
fpConePointer(aShapeCurves,nLength,120);
elseif sPointerType == "ArrowPointer" then
table.insert(aShapeCurves,fpLineCurve(0,0,0,-nLength,0));
bDrawArrow = true;
end
-- Rotate and Position the Pointer
for nIndex,aCurve in ipairs(aShapeCurves) do
for nPointIndex,aPoint in ipairs(aCurve) do
aCurve[nPointIndex] = {nXCoord,nYCoord};
end
end

end};

Another exapmle is the Savage Worlds "Teardrop" Pointer/Template, which is simply two Lines joined by two Regular Curves - because the bottom Curve (the large one) covers more than 180-Degrees I'd draw that as two smaller Curves each starting at the Y-Axis and curving up to the left and right, and while the top Curve is less than 180-Degrees and so could be drawn "as is" by either of the Curve Functions, I'd probably draw it as two Curves as well, again, starting at the Y-Axis (actually the Origin) and curving down to the left and the right. The two LineCurves, obviously, would join these Curves.

The File - Pointer_Toolkit.lua

Feel free to copy and use this code as you like, but please include the copyright information found at the top.

Code:

--

-- © Copyright Matthew James BLACK 2005-13 except where explicitly stated otherwise.
-- Fantasy Grounds is Copyright © 2004-2012 SmiteWorks USA LLC.
-- Copyright to other material within this file may be held by other Individuals and/or Entities.
-- Nothing in or from this LUA file in printed, electronic and/or any other form may be used, copied,
--    transmitted or otherwise manipulated in ANY way without the explicit written consent of Matthew
--    James BLACK or, where applicable, any and all other Copyright holders.
--

function onBuildCustomPointer(nStartXCoord,nStartYCoord,nEndXCoord,nEndYCoord,sPointerType)
local nLength = math.sqrt((nEndXCoord-nStartXCoord)^2+(nEndYCoord-nStartYCoord)^2);
if nLength == 0 then
return
end
local aShapeCurves = {};
local aDistLabelPosition = {25,25};
local bDrawArrow = false;
local nAngleRadians = math.atan2(nEndXCoord-nStartXCoord,nStartYCoord-nEndYCoord);
-- Call the relevant Pointer Definition Function
-- Sample PointerTypes Shown
if sPointerType == "CirclePointerAsEllipse" then
fpEllipsePointer(aShapeCurves,nLength,nLength,0);
elseif sPointerType == "CirclePointerAsArcs" then
fpCirclePointer(aShapeCurves,nLength);
elseif sPointerType == "HalfWidthEllipsePointerCenterOrigin" then
fpEllipsePointer(aShapeCurves,nLength/2,nLength,0);
elseif sPointerType == "HalfWidthEllipsePointerStartPointOrigin" then
fpEllipsePointer(aShapeCurves,nLength/4,nLength/2,nLength/2);
elseif sPointerType == "SquarePointer" then
fpBoxPointer(aShapeCurves,nLength,nLength,0);
elseif sPointerType == "DoubleWidthBoxPointerCenterOrigin" then
fpBoxPointer(aShapeCurves,nLength,nLengthx2,0);
elseif sPointerType == "DWBoxPointerStartPointOrigin" then
fpBoxPointer(aShapeCurves,nLength/2,nLength,nLength/2);
elseif sPointerType == "ConePointer" then
fpConePointer(aShapeCurves,nLength,90);
elseif sPointerType == "60ConePointer" then
fpConePointer(aShapeCurves,nLength,60);
elseif sPointerType == "120ConePointer" then
fpConePointer(aShapeCurves,nLength,120);
elseif sPointerType == "ArrowPointer" then
table.insert(aShapeCurves,fpLineCurve(0,0,0,-nLength,0));
bDrawArrow = true;
end
-- Rotate and Position the Pointer
for nIndex,aCurve in ipairs(aShapeCurves) do
for nPointIndex,aPoint in ipairs(aCurve) do
aCurve[nPointIndex] = {nXCoord,nYCoord};
end
end
end

-- Pointer Definition Functions

function fpBoxPointer(aShapeCurves,nLength,nWidth,nOffset)
-- Draw a Rectangle offset in the Negative-Y direction by nOffset.
table.insert(aShapeCurves,fpLineCurve(nWidth,nLength,-nWidth,nLength,nOffset));
table.insert(aShapeCurves,fpLineCurve(-nWidth,nLength,-nWidth,-nLength,nOffset));
table.insert(aShapeCurves,fpLineCurve(-nWidth,-nLength,nWidth,-nLength,nOffset));
table.insert(aShapeCurves,fpLineCurve(nWidth,-nLength,nWidth,nLength,nOffset));
end

-- Draw a Circle of Radius nRadius made up of eight Regular Arcs.
end

-- Draw a Cone with a Radius of nRadius and covering an Arc of nArcDegrees.
if nArcDegrees == 0 or
nArcDegrees <= -180 or
nArcDegrees >= 180 then
return;
end
table.insert(aShapeCurves,fpLineCurve(0,0,nXCoord,-nYCoord,0));
table.insert(aShapeCurves,fpLineCurve(0,0,-nXCoord,-nYCoord,0));
end

-- Draw an Ellipse offset in the Negative-Y direction by nOffset.
end

-- Curve Definition Functions

function fpAngleArcCurve(nStartCurveXCoord,nStartCurveYCoord,nCurveCentreXCoord,nCurveCentreYCoord,nArcDegrees)
-- Draw a Circular Arc covering nArcDegreess (-180 < nArcDegrees < 180, nArcDegrees ~= 0) given
--        the Circle Centre and the Arc Start Point.
if nArcDegrees == 0 or
nArcDegrees <= -180 or
nArcDegrees >= 180 then
return;
end
local nRadius = math.sqrt((nCurveCentreXCoord-nStartCurveXCoord)^2+(nCurveCentreYCoord-nStartCurveYCoord)^2);
if nRadius == 0 then
return;
end
end

function fpEndpointArcCurve(nStartCurveXCoord,nStartCurveYCoord,nCurveCentreXCoord,nCurveCentreYCoord,nEndCurveXCoord,nEndCurveYCoord)
-- Draw a Circular Arc given the Circle Centre, the Arc Start Point and the Arc End Point.
local nStartAngleRadians = math.atan2(nStartCurveXCoord-nCurveCentreXCoord,nCurveCentreYCoord-nStartCurveYCoord);
local nEndAngleRadians = math.atan2(nEndCurveXCoord-nCurveCentreXCoord,nCurveCentreYCoord-nEndCurveYCoord);
if nArcRadians == 0 or
return;
end
local nRadius = math.sqrt((nCurveCentreXCoord-nStartCurveXCoord)^2+(nCurveCentreYCoord-nStartCurveYCoord)^2);
if nRadius == 0 then
return;
end
end

-- Draw an Regular Arc (of a Circle) of Radius nRadius with an Origin of (0,0) and covering
--        an Arc of nArcRadians (in Radians) bisected by the Positive X-Axis and then Rotated
--        around the Origin by an angle of nAngleRadians (in Radians) and offset in both the
--        X-Direction and Y-Direction by nCurveCentreXCoord and nCurveCentreYCoord respectively.
local nX = math.cos(nArcRadians/2);
local nY = math.sin(nArcRadians/2);
local nStartX = nXxnRadius;
local nStartY = nYxnRadius;
local nControlX = nRadiusx(4-nX)/3;
local nControlY = nRadiusx(1-nX)x(3-nX)/(3xnY);
local aCurve = {{nStartX,nStartY},
{nControlX,nControlY},
{nControlX,-nControlY},
{nStartX,-nStartY}};
for nPointIndex,aPoint in ipairs(aCurve) do
aCurve[nPointIndex] = {nXCoord,nYCoord};
end
return aCurve;
end

-- Draw a 90-Degree Arc of an Ellipse offset in the Negative-Y direction by nOffset.
local nKappa = 4/3x(math.sqrt(2)-1);
local aCurve = {{0,nYRadius-nOffset},
return aCurve;
end

function fpLineCurve(nStartLineXCoord,nStartLineYCoord,nEndLineXCoord,nEndLineYCoord,nOffset)
-- Draw a Line offset in the Negative-Y direction by nOffset.
local aCurve = {{nStartLineXCoord,nStartLineYCoord-nOffset},
{nStartLineXCoord,nStartLineYCoord-nOffset},
{nEndLineXCoord,nEndLineYCoord-nOffset},
{nEndLineXCoord,nEndLineYCoord-nOffset}};
return aCurve;

end};