-
July 26th, 2013, 18:38 #1
Custom Pointers Coding Toolkit - Over Several Posts
Hi Kids,
There's a little known and (relatively) un-Documented (until now) feature of FG2 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.
Background/Theory
The Default Pointers included with FG2 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 FG2 are 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 FG2 the four points 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 } };
Code:table.insert(aShape,aCurve1); table.insert(aShape,aCurve2); etc.
Dulux-Oz
√(-1) 2^3 Σ Π
...And it was Delicious!
Alpha-Geek
ICT Professional
GMing Since 1982
NSW, Australia, UTC +10
LinkedIn Profile: www.linkedin.com/in/mjblack
Watch our games on Twitch: www.twitch.tv/dulux_oz
Support Me on Patreon: www.patreon.com/duluxoz
Past Games, etc, on my YouTube Channel: www.youtube.com/c/duluxoz
-
July 26th, 2013, 18:45 #2
Part 2
Continuing on:
Usage
To access the Custom Pointer functionality we need to add the following code to our ImageControl:
Code:<imagecontrol> <pointertypes> <custom name="45Cone"> <icon>pointer_cone</icon> <label>Draw a 45-Degree Cone</label> </custom> </pointertypes> </imagecontrol>
By default, the four Default Pointers which come with FG2 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> </imagecontrol>
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 (all numerics) and the Custom Pointer Type (a string that matches the "name" attribute of the Custom tag). 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 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" /> </imagecontrol>
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 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
Code:function fpEllipseCurve(nXRadius,nYRadius,nOffset) -- Draw a 90-Degree Arc of an Ellipse offset in the Negative-Y direction by nOffset. local nKappa = 4/3*(math.sqrt(2)-1); local aCurve = {{0,nYRadius-nOffset}, {nXRadius*nKappa,nYRadius-nOffset}, {nXRadius,nYRadius*nKappa-nOffset}, {nXRadius,-nOffset}}; return aCurve; end
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 nArcRadians = math.rad(nArcDegrees); local nRadius = math.sqrt((nCurveCentreXCoord-nStartCurveXCoord)^2+(nCurveCentreYCoord-nStartCurveYCoord)^2); if nRadius == 0 then return; end local nAngleRadians = math.atan2(nStartCurveXCoord-nCurveCentreXCoord,nCurveCentreYCoord-nStartCurveYCoord)+nArcRadians/2; return fpArcCurve(nAngleRadians,nArcRadians,nRadius,nCurveCentreXCoord,nCurveCentreYCoord); 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); local nArcRadians = math.abs(nStartAngleRadians-nEndAngleRadians) if nArcRadians == 0 or nArcRadians <= math.rad(-180) or nArcRadians >= math.rad(180) then return; end local nRadius = math.sqrt((nCurveCentreXCoord-nStartCurveXCoord)^2+(nCurveCentreYCoord-nStartCurveYCoord)^2); if nRadius == 0 then return; end local nAngleRadians = math.atan2(nStartCurveXCoord-nCurveCentreXCoord,nCurveCentreYCoord-nStartCurveYCoord)+nArcRadians/2; return fpArcCurve(nAngleRadians,nArcRadians,nRadius,nCurveCentreXCoord,nCurveCentreYCoord); end function fpArcCurve(nAngleRadians,nArcRadians,nRadius,nCurveCentreXCoord,nCurveCentreYCoord) -- 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 = nX*nRadius; local nStartY = nY*nRadius; local nControlX = nRadius*(4-nX)/3; local nControlY = nRadius*(1-nX)*(3-nX)/(3*nY); local aCurve = {{nStartX,nStartY}, {nControlX,nControlY}, {nControlX,-nControlY}, {nStartX,-nStartY}}; for nPointIndex,aPoint in ipairs(aCurve) do local nXCoord = (aPoint[1]-nCurveCentreXCoord)*math.cos(nAngleRadians)-(aPoint[2]-nCurveCentreYCoord)*math.sin(nAngleRadians)+nCurveCentreXCoord; local nYCoord = (aPoint[1]-nCurveCentreXCoord)*math.sin(nAngleRadians)+(aPoint[2]-nCurveCentreYCoord)*math.cos(nAngleRadians)+nCurveCentreYCoord; aCurve[nPointIndex] = {nXCoord,nYCoord}; end return aCurve; end
Last edited by dulux-oz; July 27th, 2013 at 04:26.
Dulux-Oz
√(-1) 2^3 Σ Π
...And it was Delicious!
Alpha-Geek
ICT Professional
GMing Since 1982
NSW, Australia, UTC +10
LinkedIn Profile: www.linkedin.com/in/mjblack
Watch our games on Twitch: www.twitch.tv/dulux_oz
Support Me on Patreon: www.patreon.com/duluxoz
Past Games, etc, on my YouTube Channel: www.youtube.com/c/duluxoz
-
July 26th, 2013, 18:49 #3
Part 3
Continuing on:
So, how do you use these Curve Definition Functions? Well, I've included some 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
Code:function fpCirclePointer(aShapeCurves,nRadius) -- Draw a Circle of Radius nRadius made up of eight Regular Arcs. local nDegreesInRadians = math.rad(45); table.insert(aShapeCurves,fpAngleArcCurve(0,nRadius,0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(nRadius*math.sin(nDegreesInRadians),nRadius*math.cos(nDegreesInRadians),0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(nRadius,0,0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(-nRadius*math.sin(nDegreesInRadians),nRadius*math.cos(nDegreesInRadians),0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(0,-nRadius,0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(-nRadius*math.sin(nDegreesInRadians),-nRadius*math.cos(nDegreesInRadians),0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(-nRadius,0,0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(nRadius*math.sin(nDegreesInRadians),-nRadius*math.cos(nDegreesInRadians),0,0,45)); end
Code:function fpConePointer(aShapeCurves,nRadius,nArcDegrees) -- 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 local nArcRadians = math.rad(nArcDegrees); local nXCoord = -nRadius*math.sin(nArcRadians/2); local nYCoord = nRadius*math.cos(nArcRadians/2); table.insert(aShapeCurves,fpLineCurve(0,0,nXCoord,-nYCoord,0)); table.insert(aShapeCurves,fpLineCurve(0,0,-nXCoord,-nYCoord,0)); table.insert(aShapeCurves,fpAngleArcCurve(-nRadius*math.cos(nArcRadians/2),nRadius*math.sin(nArcRadians/2),0,0,nArcDegrees)); end
Code:function fpEllipsePointer(aShapeCurves,nXRadius,nYRadius,nOffset) -- Draw an Ellipse offset in the Negative-Y direction by nOffset. table.insert(aShapeCurves,fpEllipseCurve(nXRadius,nYRadius,nOffset)); table.insert(aShapeCurves,fpEllipseCurve(-nXRadius,nYRadius,nOffset)); table.insert(aShapeCurves,fpEllipseCurve(-nXRadius,-nYRadius,nOffset)); table.insert(aShapeCurves,fpEllipseCurve(nXRadius,-nYRadius,nOffset)); end
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,nLength*2,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 local nXCoord = aPoint[1]*math.cos(nAngleRadians)-aPoint[2]*math.sin(nAngleRadians)+nStartXCoord; local nYCoord = aPoint[1]*math.sin(nAngleRadians)+aPoint[2]*math.cos(nAngleRadians)+nStartYCoord; aCurve[nPointIndex] = {nXCoord,nYCoord}; end end return aShapeCurves,aDistLabelPosition,bDrawArrow; end
Last edited by dulux-oz; July 26th, 2013 at 18:52.
Dulux-Oz
√(-1) 2^3 Σ Π
...And it was Delicious!
Alpha-Geek
ICT Professional
GMing Since 1982
NSW, Australia, UTC +10
LinkedIn Profile: www.linkedin.com/in/mjblack
Watch our games on Twitch: www.twitch.tv/dulux_oz
Support Me on Patreon: www.patreon.com/duluxoz
Past Games, etc, on my YouTube Channel: www.youtube.com/c/duluxoz
-
July 26th, 2013, 18:56 #4
Part 4
Finally:
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,nLength*2,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 local nXCoord = aPoint[1]*math.cos(nAngleRadians)-aPoint[2]*math.sin(nAngleRadians)+nStartXCoord; local nYCoord = aPoint[1]*math.sin(nAngleRadians)+aPoint[2]*math.cos(nAngleRadians)+nStartYCoord; aCurve[nPointIndex] = {nXCoord,nYCoord}; end end return aShapeCurves,aDistLabelPosition,bDrawArrow; 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 function fpCirclePointer(aShapeCurves,nRadius) -- Draw a Circle of Radius nRadius made up of eight Regular Arcs. local nDegreesInRadians = math.rad(45); table.insert(aShapeCurves,fpAngleArcCurve(0,nRadius,0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(nRadius*math.sin(nDegreesInRadians),nRadius*math.cos(nDegreesInRadians),0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(nRadius,0,0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(-nRadius*math.sin(nDegreesInRadians),nRadius*math.cos(nDegreesInRadians),0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(0,-nRadius,0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(-nRadius*math.sin(nDegreesInRadians),-nRadius*math.cos(nDegreesInRadians),0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(-nRadius,0,0,0,45)); table.insert(aShapeCurves,fpAngleArcCurve(nRadius*math.sin(nDegreesInRadians),-nRadius*math.cos(nDegreesInRadians),0,0,45)); end function fpConePointer(aShapeCurves,nRadius,nArcDegrees) -- 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 local nArcRadians = math.rad(nArcDegrees); local nXCoord = -nRadius*math.sin(nArcRadians/2); local nYCoord = nRadius*math.cos(nArcRadians/2); table.insert(aShapeCurves,fpLineCurve(0,0,nXCoord,-nYCoord,0)); table.insert(aShapeCurves,fpLineCurve(0,0,-nXCoord,-nYCoord,0)); table.insert(aShapeCurves,fpAngleArcCurve(-nRadius*math.cos(nArcRadians/2),nRadius*math.sin(nArcRadians/2),0,0,nArcDegrees)); end function fpEllipsePointer(aShapeCurves,nXRadius,nYRadius,nOffset) -- Draw an Ellipse offset in the Negative-Y direction by nOffset. table.insert(aShapeCurves,fpEllipseCurve(nXRadius,nYRadius,nOffset)); table.insert(aShapeCurves,fpEllipseCurve(-nXRadius,nYRadius,nOffset)); table.insert(aShapeCurves,fpEllipseCurve(-nXRadius,-nYRadius,nOffset)); table.insert(aShapeCurves,fpEllipseCurve(nXRadius,-nYRadius,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 nArcRadians = math.rad(nArcDegrees); local nRadius = math.sqrt((nCurveCentreXCoord-nStartCurveXCoord)^2+(nCurveCentreYCoord-nStartCurveYCoord)^2); if nRadius == 0 then return; end local nAngleRadians = math.atan2(nStartCurveXCoord-nCurveCentreXCoord,nCurveCentreYCoord-nStartCurveYCoord)+nArcRadians/2; return fpArcCurve(nAngleRadians,nArcRadians,nRadius,nCurveCentreXCoord,nCurveCentreYCoord); 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); local nArcRadians = math.abs(nStartAngleRadians-nEndAngleRadians) if nArcRadians == 0 or nArcRadians <= math.rad(-180) or nArcRadians >= math.rad(180) then return; end local nRadius = math.sqrt((nCurveCentreXCoord-nStartCurveXCoord)^2+(nCurveCentreYCoord-nStartCurveYCoord)^2); if nRadius == 0 then return; end local nAngleRadians = math.atan2(nStartCurveXCoord-nCurveCentreXCoord,nCurveCentreYCoord-nStartCurveYCoord)+nArcRadians/2; return fpArcCurve(nAngleRadians,nArcRadians,nRadius,nCurveCentreXCoord,nCurveCentreYCoord); end function fpArcCurve(nAngleRadians,nArcRadians,nRadius,nCurveCentreXCoord,nCurveCentreYCoord) -- 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 = nX*nRadius; local nStartY = nY*nRadius; local nControlX = nRadius*(4-nX)/3; local nControlY = nRadius*(1-nX)*(3-nX)/(3*nY); local aCurve = {{nStartX,nStartY}, {nControlX,nControlY}, {nControlX,-nControlY}, {nStartX,-nStartY}}; for nPointIndex,aPoint in ipairs(aCurve) do local nXCoord = (aPoint[1]-nCurveCentreXCoord)*math.cos(nAngleRadians)-(aPoint[2]-nCurveCentreYCoord)*math.sin(nAngleRadians)+nCurveCentreXCoord; local nYCoord = (aPoint[1]-nCurveCentreXCoord)*math.sin(nAngleRadians)+(aPoint[2]-nCurveCentreYCoord)*math.cos(nAngleRadians)+nCurveCentreYCoord; aCurve[nPointIndex] = {nXCoord,nYCoord}; end return aCurve; end function fpEllipseCurve(nXRadius,nYRadius,nOffset) -- Draw a 90-Degree Arc of an Ellipse offset in the Negative-Y direction by nOffset. local nKappa = 4/3*(math.sqrt(2)-1); local aCurve = {{0,nYRadius-nOffset}, {nXRadius*nKappa,nYRadius-nOffset}, {nXRadius,nYRadius*nKappa-nOffset}, {nXRadius,-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
Last edited by dulux-oz; July 27th, 2013 at 04:25.
Dulux-Oz
√(-1) 2^3 Σ Π
...And it was Delicious!
Alpha-Geek
ICT Professional
GMing Since 1982
NSW, Australia, UTC +10
LinkedIn Profile: www.linkedin.com/in/mjblack
Watch our games on Twitch: www.twitch.tv/dulux_oz
Support Me on Patreon: www.patreon.com/duluxoz
Past Games, etc, on my YouTube Channel: www.youtube.com/c/duluxoz
-
July 26th, 2013, 20:17 #5
- Join Date
- May 2013
- Location
- East Coast USA.
- Posts
- 945
I know what splines are and I find the mathematical information interesting, but I'm not sure exactly what this does for the user.
Edit: I'm not trying to minimize the usefulness, I honestly am not sure what what this allows.
-
July 26th, 2013, 23:44 #6
nothing for the user - it allows the GM to create new pointers for use within his games
-
July 29th, 2013, 14:33 #7
- Join Date
- Mar 2007
- Posts
- 477
Silly Damned! GMs are users, too! Very cool, dulux! thanks for the in-depth info. will have to see if I can figure out how to make the toolkit work for me.
Cheers!
GMK
-
July 29th, 2013, 16:18 #8
You're welcome!
If you have any questions or what some help feel free to drop me a line.
What do people reckon - is it worth asking the FG2 Forum Moderators about making this thread "sticky"? I mean, if people think it's usesful stuff <shrug>
Or is it just my rampant ego taking over again <grin>Last edited by dulux-oz; July 29th, 2013 at 16:30.
Dulux-Oz
√(-1) 2^3 Σ Π
...And it was Delicious!
Alpha-Geek
ICT Professional
GMing Since 1982
NSW, Australia, UTC +10
LinkedIn Profile: www.linkedin.com/in/mjblack
Watch our games on Twitch: www.twitch.tv/dulux_oz
Support Me on Patreon: www.patreon.com/duluxoz
Past Games, etc, on my YouTube Channel: www.youtube.com/c/duluxoz
-
July 29th, 2013, 20:35 #9
Hmmm... I too am unsure what the usefulness of this info is... which is an indicator of my lack of knowledge, not the usefulness of the info! I'm *just* getting my sea legs in FG dev and am not sure what a "pointer" is in FG terms, and how Bezier curves apply to them. Is this used for graphical output or for plotting movement paths or... ? Clearly, I have a lesson to learn here in FG dev! Thanks, dulux-oz!!!
"A ship in harbor is safe, but that is not what ships are built for." - John Shedd
"Why is it every time we need to get somewhere, I get waylaid by jackassery?" - Dr. Thaddeus Venture
-- CA (Pacific time zone) --
-
July 30th, 2013, 04:02 #10
Hmm, OK, obviously I broke one of my own cardinal rules with this thread - "Assume No Knowledge".
A Pointer in FG2 is the Circle, Square, Cone and Arrow shapes we draw on our Images when we are gaming - for Spell Effect areas, etc. The FG2 Documentatuion calls them Pointers (under the "Maps, Images and Drawings" Heading) so I assumed that's what we all called them.
If you have any knowledge of programming you will have come across the term "pointer" before - these are a special logical construction which have NO relationship to what we are talking about here (I don't even know why I brought this up ).
These shapes (the Circle, Square, Cone and Arrow) are drawn on our images using mathemetatical equations which are known as Cubic Bézier Equations which, when plotted on a graph (ie drawn) form Cubic Bézier Curves. Add a series of such Curves together (following the rule in the 7th paragraph of the Background/Theory part of the 1st post) and you get a Spline - a smoothly flowing series of curves. Add several Splines together on the same drawing and, in the case of FG2, we get our Pointers.
The Toolkit simply makes drawing these Curves easier so that we can have Pointers on our Images & Maps which are not the stock-standard ones. If you are not using custom Pointers then the Toolkit will be of no use to you.
However, if you do want to use "non-standard" Pointers (as I do for my Übergame Ruleset) then instead of having to learn all the math involved and then solve the cubic equations (ie in the form of y = Ax^3 + Bx^2 + Cx + D) I've done the math for you - well, I've simplified things down so that you only have to supply a few coordinates and maybe a radius or two. I happen to be a Mathematicial as well as a senior ICT Professional, so, for me, the maths is realtively easy, but for othes it tends to be quite hard and confusing - not something that the vast majority of people know or care about.
I put the Background/Theory section in so that people could:
- Do some further reaseach if they were interested.
- Understand what we were actually trying to acheive.
- Know the limitations of the Toolkit and how they might modify things to do things the Toolkit does not do explicitly.
My belief is that if people have as much information as possible its better than if they have little/none.
So if people find this Toolkit useful then that's great; if they don't then that's OK too. I just thought I'd help out with something that a lot of people (inside and outside the FG2 community) seem to have trouble with - God knows I've had trouble with other things in FG2 and the community has always "come to my rescue", so I thought I'd do a little "payback" as a sort of "thankyou".Dulux-Oz
√(-1) 2^3 Σ Π
...And it was Delicious!
Alpha-Geek
ICT Professional
GMing Since 1982
NSW, Australia, UTC +10
LinkedIn Profile: www.linkedin.com/in/mjblack
Watch our games on Twitch: www.twitch.tv/dulux_oz
Support Me on Patreon: www.patreon.com/duluxoz
Past Games, etc, on my YouTube Channel: www.youtube.com/c/duluxoz
Thread Information
Users Browsing this Thread
There are currently 1 users browsing this thread. (0 members and 1 guests)
Bookmarks