Page 1 of 9 123 ... Last
  1. #1
    dulux-oz's Avatar
    Join Date
    Jan 2012
    Location
    Australia
    Posts
    4,056
    Blog Entries
    14

    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 }
        };
    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.
    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

    Read my FG Blog here.

    Watch our games on Twitch: www.twitch.tv/dulux_oz

    Support Ongoing Video, Ruleset & Extension Development: via PayPal (Send To: [email protected])

    YouTube Channel/Tutorial Playlists: www.youtube.com/c/duluxoz

  2. #2
    dulux-oz's Avatar
    Join Date
    Jan 2012
    Location
    Australia
    Posts
    4,056
    Blog Entries
    14

    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>
    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 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>
    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 (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>
    When working with the code I realised that, thanks to the pioneering work of Tero Parvinen (one of the FG2 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 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). 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:
    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
    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 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 05: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

    Read my FG Blog here.

    Watch our games on Twitch: www.twitch.tv/dulux_oz

    Support Ongoing Video, Ruleset & Extension Development: via PayPal (Send To: [email protected])

    YouTube Channel/Tutorial Playlists: www.youtube.com/c/duluxoz

  3. #3
    dulux-oz's Avatar
    Join Date
    Jan 2012
    Location
    Australia
    Posts
    4,056
    Blog Entries
    14

    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
    The second ( fpCirclePointer() ) draws a replica of the default Circle Pointer with a given Radius and is made up of eight equal Arcs.

    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
    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:
    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
    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:
    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
    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,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 19: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

    Read my FG Blog here.

    Watch our games on Twitch: www.twitch.tv/dulux_oz

    Support Ongoing Video, Ruleset & Extension Development: via PayPal (Send To: [email protected])

    YouTube Channel/Tutorial Playlists: www.youtube.com/c/duluxoz

  4. #4
    dulux-oz's Avatar
    Join Date
    Jan 2012
    Location
    Australia
    Posts
    4,056
    Blog Entries
    14

    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 05: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

    Read my FG Blog here.

    Watch our games on Twitch: www.twitch.tv/dulux_oz

    Support Ongoing Video, Ruleset & Extension Development: via PayPal (Send To: [email protected])

    YouTube Channel/Tutorial Playlists: www.youtube.com/c/duluxoz

  5. #5
    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.

  6. #6
    damned's Avatar
    Join Date
    Mar 2011
    Location
    Australia
    Posts
    19,382
    Blog Entries
    1
    nothing for the user - it allows the GM to create new pointers for use within his games

    MoreCore - Generic Ruleset
    --- Projects ---
    Extensions | Tutorials | MoreCore | MoreCore Themes | Call of Cthulhu | Maelstrom | FG Con

  7. #7
    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

  8. #8
    dulux-oz's Avatar
    Join Date
    Jan 2012
    Location
    Australia
    Posts
    4,056
    Blog Entries
    14
    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 17: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

    Read my FG Blog here.

    Watch our games on Twitch: www.twitch.tv/dulux_oz

    Support Ongoing Video, Ruleset & Extension Development: via PayPal (Send To: [email protected])

    YouTube Channel/Tutorial Playlists: www.youtube.com/c/duluxoz

  9. #9
    dr_venture's Avatar
    Join Date
    Apr 2009
    Location
    Yosemite, CA
    Posts
    1,126
    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) --

  10. #10
    dulux-oz's Avatar
    Join Date
    Jan 2012
    Location
    Australia
    Posts
    4,056
    Blog Entries
    14
    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:
    1. Do some further reaseach if they were interested.
    2. Understand what we were actually trying to acheive.
    3. 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

    Read my FG Blog here.

    Watch our games on Twitch: www.twitch.tv/dulux_oz

    Support Ongoing Video, Ruleset & Extension Development: via PayPal (Send To: [email protected])

    YouTube Channel/Tutorial Playlists: www.youtube.com/c/duluxoz

Thread Information

Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  

Log in

Log in