No idea if it's the best way or not, but I use a recursive approach. This is a very stripped-down example adapted from my personal Spline library. It can handle any number of points. The tangent vector is visualized as a yellow line running across the spline. Note that it's not normalized in this example, so its magnitude varies across the curve. I did my best to explain what's happening in the comments.
UseNewDefaultFonts(1)
SetVirtualResolution(1024,768)
type Spline
points as SplinePoint[] // list of point positions
evalList as SplinePoint[] // list of interpolated points
evalX as float // the x position of the last evaluation
evalY as float // the y position of the last evaluation
evalZ as float // the z position of the last evaluation
evalTangentX as float // the non-normalized tangent vector x of the last evaluation
evalTangentY as float // the non-normalized tangent vector y of the last evaluation
evalTangentZ as float // the non-normalized tangent vector z of the last evaluation
endtype
type SplinePoint
x as float // x position
y as float // y position
z as float // z position
endtype
// just creating some images to visualize the spline points and curve
pointImg = CreateImageColor(0,255,0,255)
curveImg = CreateImageColor(0,128,128,255)
// creating a new spline and adding some points to it. Also creating sprites for visualization.
testSpline as Spline
testSpline = CreateSpline()
for i = 0 to 3 // change the 3 to another number to increase/decrease points
spr = CreateSprite(pointImg)
SetSpriteSize(spr,16,16)
SetSpritePosition(spr,random(0,GetVirtualWidth()), random(0,GetVirtualHeight()))
AddSplinePoint(testSpline, GetSpriteX(spr) + 8, GetSpriteY(spr) + 8, 0)
next i
// evaluating the spline in increments of 0.005 and generating sprites at the resulting
// coordinates to visualize the curve.
t# = 0.0
while t# <= 1.0
EvaluateSpline(testSpline, t#)
spr = CreateSprite(curveImg)
SetSpriteSize(spr,4,4)
SetSpritePosition(spr,GetSplineEvalX(testSpline),GetSplineEvalY(testSpline))
inc t#, 0.005
endwhile
do
// just drawing some lines between the points to get a better idea of their order
for i = 0 to testSpline.points.length - 1
DrawLine(testSpline.points[i].x, testSpline.points[i].y, testSpline.points[i+1].x, testSpline.points[i+1].y, 0xAAAAAA, 0xAAAAAA)
next i
// visualizing the tangent vector at t#.
// note that the tangent vector is not normalized in this example, so it changes magnitude (length)
t# = t# + 0.005
if t# > 1.0 then t# = 0.0
EvaluateSpline(testSpline, t#)
DrawLine(GetSplineEvalX(testSpline), GetSplineEvalY(testSpline), GetSplineEvalX(testSpline) + GetSplineEvalTangentX(testSpline), GetSplineEvalY(testSpline) + GetSplineEvalTangentY(testSpline), 0x00FFFF, 0x00FFFF)
Print( ScreenFPS() )
Sync()
loop
function CreateSpline()
sp as Spline
endfunction sp
function AddSplinePoint(sp ref as Spline, x#, y#, z#)
point as SplinePoint
point.x = x#
point.y = y#
point.z = z#
sp.points.insert(point)
endfunction
function EvaluateSpline(sp ref as Spline, t#)
// if the spline has no points, there is no need to evaluate.
if sp.points.length = -1 then exitfunction
// if the spline has one point, just set the evaluation coordinates to it's position
if sp.points.length = 0
sp.evalX = sp.points[0].x
sp.evalY = sp.points[0].y
sp.evalZ = sp.points[0].z
exitfunction
endif
// if the spline has only 2 points, interpolate linearly.
if sp.points.length = 1
sp.evalX = LerpFloat(sp.points[0].x, sp.points[1].x, t#)
sp.evalY = LerpFloat(sp.points[0].y, sp.points[1].y, t#)
sp.evalZ = LerpFloat(sp.points[0].z, sp.points[1].z, t#)
sp.evalTangentX = sp.points[1].x - sp.points[0].x
sp.evalTangentY = sp.points[1].y - sp.points[0].y
sp.evalTangentZ = sp.points[1].z - sp.points[0].z
exitfunction
endif
// if the spline has more than 2 points, we need to get new interpolated points.
newPoint as SplinePoint // creating a new SplinePoint var to hold the interpolated point data
// if the spline's evaluation list has no points, we need to use the main points list to get
// our first set of interpolated points.
// Imagine lines drawn between each of the main points. Now imagine new points being placed along
// those lines. If t# = 0.5, then the points would be placed at 50% along the lines. These new points
// are our interpolated points, which we'll insert into the evaluation list.
if sp.evalList.length = -1
for i = 1 to sp.points.length
newPoint.x = LerpFloat(sp.points[i-1].x, sp.points[i].x, t#)
newPoint.y = LerpFloat(sp.points[i-1].y, sp.points[i].y, t#)
newPoint.z = LerpFloat(sp.points[i-1].z, sp.points[i].z, t#)
sp.evalList.insert(newPoint)
next i
endif
// if the evaluation list has more than 2 points, we're basically doing the same as above, only
// we're using the evaluation list to get our next set of interpolated points instead of the main
// points list. Again, imagine lines being drawn between the points in the evaluation list, and new
// points being placed along those lines at t#.
// We do this recursively until we're left with an evaluation list that has only 2 points.
if sp.evalList.length > 1
copyList as SplinePoint[] // creating a new list to copy the current contents of the evaluation list
copyList = sp.evalList
sp.evalList.length = -1 // clearing the current contents of the evaluation list, so we can fill it with
// the next set of interpolated points.
for i = 1 to copyList.length
newPoint.x = LerpFloat(copyList[i-1].x, copyList[i].x, t#)
newPoint.y = LerpFloat(copyList[i-1].y, copyList[i].y, t#)
newPoint.z = LerpFloat(copyList[i-1].z, copyList[i].z, t#)
sp.evalList.insert(newPoint)
next i
// recurse until sp.evalList.length = 1
EvaluateSpline(sp, t#)
else
sp.evalX = LerpFloat(sp.evalList[0].x, sp.evalList[1].x, t#)
sp.evalY = LerpFloat(sp.evalList[0].y, sp.evalList[1].y, t#)
sp.evalZ = LerpFloat(sp.evalList[0].z, sp.evalList[1].z, t#)
// the tangent vector is in the same direction as the line between the the last 2 interpolation points.
sp.evalTangentX = sp.evalList[1].x - sp.evalList[0].x
sp.evalTangentY = sp.evalList[1].y - sp.evalList[0].y
sp.evalTangentZ = sp.evalList[1].z - sp.evalList[0].z
sp.evalList.length = -1 // empty the evaluation list when we're done evaluating.
endif
endfunction
// return the x position of the last evaluation call
function GetSplineEvalX(sp ref as Spline)
endfunction sp.evalX
// return the y position of the last evaluation call
function GetSplineEvalY(sp ref as Spline)
endfunction sp.evalY
// return the z position of the last evaluation call
function GetSplineEvalZ(sp ref as Spline)
endfunction sp.evalZ
// return the x component of the tangent vector from the last evaluation call
function GetSplineEvalTangentX(sp ref as Spline)
endfunction sp.evalTangentX
// return the y component of the tangent vector from the last evaluation call
function GetSplineEvalTangentY(sp ref as Spline)
endfunction sp.evalTangentY
// return the z component of the tangent vector from the last evaluation call
function GetSplineEvalTangentZ(sp ref as Spline)
endfunction sp.evalTangentZ
// helper function for getting an interpolated float value between 2 floats.
// For example, LerpFloat(100.0, 200.0, 0.5) = 150.0
function LerpFloat(a#, b#, t#)
result# = ((b# - a#) * t#) + a#
endfunction result#