Posted: 21st Nov 2021 17:58
When I finally found my old spline source code, it was written in DBP which had hermite and catmull splines built in with the vector commands so I couldn't translate that to agk. So here's a super simple implementation. It's something I needed for my tower defense game, so I'll be building on this example and will update that bit here.





Cleaner code:
+ Code Snippet
// Project: catmullrom 
// Created: 2021-11-21
// Author: Phaelax
// Source: http://www.mvps.org/directx/articles/catmull/



SetWindowSize( 1024, 768, 0 )
SetVirtualResolution( 1024, 768 )
SetSyncRate(500, 0 )


#CONSTANT segments 16	// Number of line segments between each control point (higher means smoother curve)


c = makeColor(255,255,255) // control points
cr = makecolor(255,0,0)    // spline
cg = makecolor(0,255,0)    // control point on mouse hover
cb = makecolor(0,0,255)    // current control point containing the moving point
cq = makeColor(255,255,0)  // point along the spline


Type Point2D
	x as float
	y as float
	index as float  // distance on the spline of this control point
	t as integer    // text object
EndType


dim cp[] as Point2D

P as Point2D

t# = 0.0
i = 1

cpDistance# = 0

splineDistance# = 0


ship = createSprite(loadImage("ship.png"))
setSpriteSize(ship, 84,84)
setSpriteOffset(ship, 42, 42)



movePoint = -1

do

	// Arrow keys to move ship along spline
	if getRawKeyState(37) = 1  // Left key
		dec splineDistance#, 0.75
		if splineDistance# < 0 then splineDistance# = getSplineLength(cp)
	endif
	if getRawKeyState(39) = 1  // Right key
		inc splineDistance#, 0.75
		if splineDistance# > getSplineLength(cp) then splineDistance# = 0
	endif
	
	
	// Hold right mouse to move control point
	if getRawMouseRightState() = 0 then movePoint = -1
	
	
	// Left mouse to add control point
	if getRawMouseLeftPressed()
		addControlPoint(cp, getRawMouseX(), getRawMouseY())
	endif
	
	
	// draw control points
	for j = 0 to cp.length
		if ((cp[j].x - getRawMouseX())^2 + (cp[j].y - getRawMouseY())^2 <= 100 )
			mindex = j
			color = cg
			cpDistance# = cp[j].index
			if getRawMouseRightState() = 1 and movePoint = -1 then movePoint = j
		else
			color = c
		endif
		drawEllipse(cp[j].x, cp[j].y, 10, 10, color,color,1)
		
	next j
	
	
	if movePoint > -1
		moveControlPoint(cp, movePoint, getRawMouseX(), getRawMouseY())
	endif

    
    
    // Draw spline
    drawSpline(cp, makeColor(255,0,0))
    

	// Get point on spline based on distance along its path
	oldPx# = getSpriteXByOffset(ship)
	oldPy# = getSpriteYByOffset(ship)
	P = getPointOnSpline(cp, splineDistance#)
	SetSpritePositionByOffset(ship, P.x, P.y)
	if (p.x <> oldPx# OR p.y <> oldPy#)
		a# = atanfull(p.x - oldPx#, p.y - oldPy#)-90
		setSpriteAngle(ship, a#)
	endif
			
			
			

    
    if timer() - fps# >= 0.5
		fps# = timer()
		frames = screenfps()
	endif
    `Print("FPS: "+str(frames))
    

	print("Position: "+str((round(splineDistance#))) + " / " + str(round(getSplineLength(cp))))
	if mindex > 0 then print("Dist from origin: "+str(cp[mindex].index))

    
    Sync()
loop



function getPointOnSpline(arr ref as Point2D[], distance as float)
	q as Point2D
	q.x = -1
	q.y = -1
	
	if arr.length > 3
		// Determine which control point we're closest to
		index = getIndex(arr, distance)
		// Convert distance along spline into a local distance between two control points
		localDistance# = distance - cp[index].index
		i = index
		d# = 0
		for k = 1 to segments
			if k = 1
				oldX# = cp[i].x
				oldY# = cp[i].y
			endif
			
			t# = k / (segments+0.0)
			x# = 0.5 * ((2*arr[i].x) + (-arr[i-1].x + arr[i+1].x) * t# + (2*arr[i-1].x - 5*arr[i].x + 4*arr[i+1].x - arr[i+2].x) * t#^2 + (-arr[i-1].x + 3*arr[i].x - 3*arr[i+1].x + arr[i+2].x) * t#^3)
			y# = 0.5 * ((2*arr[i].y) + (-arr[i-1].y + arr[i+1].y) * t# + (2*arr[i-1].y - 5*arr[i].y + 4*arr[i+1].y - arr[i+2].y) * t#^2 + (-arr[i-1].y + 3*arr[i].y - 3*arr[i+1].y + arr[i+2].y) * t#^3)
				
			sd# = sqrt((oldX#-x#)^2 + (oldY#-y#)^2)
			
			// Found segment, now interpolate to find exact point
			if d#+sd# > localDistance#
				t# = (localDistance# - d#) / sd#

				q.x = oldX# + (x# - oldX#)*t#
				q.y = oldY# + (y# - oldY#)*t#
				
				
				exitfunction q
			endif
			d# = d# + sd#
			
			oldX# = x#
			oldY# = y#
			
		next k
	endif
endfunction q





function addControlPoint(arr ref as Point2D[], x as float, y as float)
	p as Point2D
	p.x = x
	p.y = y
	p.t = createText("")
	setTextColor(p.t, 255,0,255,255)
	setTextSize(p.t, 30)
	setTextPosition(p.t, x+2, y+2)
	arr.insert(p)
	setTextString(arr[arr.length].t, str(arr.length))
	calculatePath(arr)
endfunction




function moveControlPoint(arr ref as Point2D[], p, x as float, y as float)
	arr[p].x = x
	arr[p].y = y
	setTextPosition(arr[p].t, x+2, y+2)
	calculatePath(arr)
endfunction




function calculatePath(arr ref as Point2D[])
	if arr.length >= 3
		d# = 0
		for i = 1 to arr.length-2
			arr[i].index = d#
			for k = 1 to segments
				if k = 1
					oldX# = arr[i].x
					oldY# = arr[i].y
				endif
				t# = k / (segments+0.0)
				
				x# = 0.5 * ((2*arr[i].x) + (-arr[i-1].x + arr[i+1].x) * t# + (2*arr[i-1].x - 5*arr[i].x + 4*arr[i+1].x - arr[i+2].x) * t#^2 + (-arr[i-1].x + 3*arr[i].x - 3*arr[i+1].x + arr[i+2].x) * t#^3)
				y# = 0.5 * ((2*arr[i].y) + (-arr[i-1].y + arr[i+1].y) * t# + (2*arr[i-1].y - 5*arr[i].y + 4*arr[i+1].y - arr[i+2].y) * t#^2 + (-arr[i-1].y + 3*arr[i].y - 3*arr[i+1].y + arr[i+2].y) * t#^3)
				
				d# = d# + sqrt((oldX#-x#)^2 + (oldY#-y#)^2) 
				oldX# = x#
				oldY# = y#
			next k
		next i
		arr[arr.length-1].index = d#  // record overall spline length in last control point
	endif
endfunction




function drawSpline(arr ref as Point2D[], color)
	
	for i = 1 to arr.length-2
		for k = 1 to segments
			if k = 1
				oldX# = arr[i].x
				oldY# = arr[i].y
			endif
			t# = k / (segments+0.0)
			
			x# = 0.5 * ((2*arr[i].x) + (-arr[i-1].x + arr[i+1].x) * t# + (2*arr[i-1].x - 5*arr[i].x + 4*arr[i+1].x - arr[i+2].x) * t#^2 + (-arr[i-1].x + 3*arr[i].x - 3*arr[i+1].x + arr[i+2].x) * t#^3)
			y# = 0.5 * ((2*arr[i].y) + (-arr[i-1].y + arr[i+1].y) * t# + (2*arr[i-1].y - 5*arr[i].y + 4*arr[i+1].y - arr[i+2].y) * t#^2 + (-arr[i-1].y + 3*arr[i].y - 3*arr[i+1].y + arr[i+2].y) * t#^3)

			drawLine(oldX#, oldY#, x#, y#, color, color)
			
			oldX# = x#
			oldY# = y#
		next k
	next i
		
endfunction
	


function getSplineLength(arr ref as Point2D[])
	if arr.length > 0
		d# = arr[arr.length-1].index
	else
		d# = -1
	endif
endfunction d#



function getIndex(arr ref as Point2D[], d#)
	if d# <= 0 then exitfunction 1
	if d# >= arr[arr.length-1].index then exitfunction arr.length-2
	
	for i = 2 to arr.length-1
		if arr[i].index > d# then exitfunction i-1
	next i
endfunction 0
	



messy code:
+ Code Snippet
 

// Project: catmullrom 
// Created: 2021-11-21
// Author: Phaelax
// Source: http://www.mvps.org/directx/articles/catmull/



SetWindowSize( 1024, 768, 0 )
SetVirtualResolution( 1024, 768 )
SetSyncRate(500, 0 )


#CONSTANT segments 20	// Number of line segments between each control point (higher means smoother curve)


c = makeColor(255,255,255) // control points
cr = makecolor(255,0,0)    // spline
cg = makecolor(0,255,0)    // control point on mouse hover
cb = makecolor(0,0,255)    // current control point containing the moving point
cq = makeColor(255,255,0)  // point along the spline


Type Point2D
	x as float
	y as float
	index as float
EndType


dim cp[10] as Point2D


for i = 0 to cp.length-1
	cp[i].x = i*100 + (random(0,200)-100)
	cp[i].y = 300 + (random(0,400)-200)
	
	`cp[i].x = random(0,1024)
	`cp[i].y = random(0,768)
next i

`cp[0] = cp[1]


t# = 0.0
i = 1

cpDistance# = 0

splineDistance# = 0


ship = createSprite(loadImage("ship.png"))
setSpriteSize(ship, 84,84)
setSpriteOffset(ship, 42, 42)





do

	// Spacebar to make new random spline
	if getRawKeyPressed(32) = 1
		for i = 0 to cp.length-1
			cp[i].x = random(0,1024)
			cp[i].y = random(0,768)
		next i

		`cp[0] = cp[1]
	endif
	
	
	if getRawKeyState(37) = 1  // Left key
		dec splineDistance#, 0.75
	endif
	if getRawKeyState(39) = 1  // Right key
		inc splineDistance#, 0.75
	endif
	if splineDistance# > cp[cp.length-1].index then splineDistance# = 0
	
	

	
    // draw control points
    for j = 0 to cp.length-1
		if ((cp[j].x - getRawMouseX())^2 + (cp[j].y - getRawMouseY())^2 <= 100 )
			mindex = j
			color = cg
			cpDistance# = cp[j].index
		else
			if j = index
				color = cb
			else
				color = c
			endif
		endif
		drawEllipse(cp[j].x, cp[j].y, 10, 10, color,color,1)
		
    next j
    
    
    
    
	// draw spline
	d# = 0
    for i = 1 to cp.length-2
		cp[i].index = d#
		for k = 1 to segments
			if k = 1
				oldX# = cp[i].x
				oldY# = cp[i].y
			endif
			t# = k / (segments+0.0)
			
			x# = 0.5 * ((2*cp[i].x) + (-cp[i-1].x + cp[i+1].x) * t# + (2*cp[i-1].x - 5*cp[i].x + 4*cp[i+1].x - cp[i+2].x) * t#^2 + (-cp[i-1].x + 3*cp[i].x - 3*cp[i+1].x + cp[i+2].x) * t#^3)
			y# = 0.5 * ((2*cp[i].y) + (-cp[i-1].y + cp[i+1].y) * t# + (2*cp[i-1].y - 5*cp[i].y + 4*cp[i+1].y - cp[i+2].y) * t#^2 + (-cp[i-1].y + 3*cp[i].y - 3*cp[i+1].y + cp[i+2].y) * t#^3)
			
			if i = index
				drawLine(oldX#, oldY#, x#, y#, cb, cb)
			else
				drawLine(oldX#, oldY#, x#, y#, cr, cr)
			endif
			
			d# = d# + sqrt((oldX#-x#)^2 + (oldY#-y#)^2) 
			oldX# = x#
			oldY# = y#
		next k
	next i
	cp[cp.length-1].index = d#  // record overall spline length in last control point
	
	


	// Find point along spline
	// Determine which control point we're closest to
	index = getIndex(cp, splineDistance#)
	// Convert distance along spline into a local distance between two control points
	localDistance# = splineDistance# - cp[index].index
	i = index
	d# = 0
	for k = 1 to segments
		if k = 1
			oldX# = cp[i].x
			oldY# = cp[i].y
		endif
		t# = k / (segments+0.0)
		
		x# = 0.5 * ((2*cp[i].x) + (-cp[i-1].x + cp[i+1].x) * t# + (2*cp[i-1].x - 5*cp[i].x + 4*cp[i+1].x - cp[i+2].x) * t#^2 + (-cp[i-1].x + 3*cp[i].x - 3*cp[i+1].x + cp[i+2].x) * t#^3)
		y# = 0.5 * ((2*cp[i].y) + (-cp[i-1].y + cp[i+1].y) * t# + (2*cp[i-1].y - 5*cp[i].y + 4*cp[i+1].y - cp[i+2].y) * t#^2 + (-cp[i-1].y + 3*cp[i].y - 3*cp[i+1].y + cp[i+2].y) * t#^3)
		sd# = sqrt((oldX#-x#)^2 + (oldY#-y#)^2)
		
		if d#+sd# > localDistance#
			t# = (localDistance# - d#) / sd#
			px# = oldX# + (x# - oldX#)*t#
			py# = oldY# + (y# - oldY#)*t#
			`drawEllipse(px#, py#, 6,6, cq,cq,1)
			a# = atanfull(x# - oldX#, y# - oldY#)-90
			SetSpritePositionByOffset(ship, px#, py#)
			setSpriteAngle(ship, a#)
			exit
		endif
		d# = d# + sd#
		
		oldX# = x#
		oldY# = y#
		
	next k
	

	
	
    
    if timer() - fps# >= 0.5
		fps# = timer()
		frames = screenfps()
	endif


    `Print("FPS: "+str(frames))
    print("Position: "+str(splineDistance#) + " / "+str(cp[cp.length-1].index))
    print("Index: "+str(index))
    print("Mouse at index: "+str(mindex))
	print("Index#: "+str(cp[mindex].index))
	
	
    
    Sync()
loop







function getIndex(arr ref as Point2D[], d#)
	if d# <= 0 then exitfunction 1
	if d# >= arr[arr.length-1].index then exitfunction arr.length-2
	
	for i = 2 to arr.length-1
		if arr[i].index > d# then exitfunction i-1
	next i
endfunction 0
	
Posted: 21st Nov 2021 19:11
Nice one. Thanks
Posted: 21st Nov 2021 19:42
Updated snippet. Shows how to traverse the path at a constant speed by calculating the overall length of the spline and angling a sprite to stay facing the path.
Posted: 21st Nov 2021 20:18
Wahoo, excellent !! thank you a lot, it's really good, not only to use it as path, but for drawing app !
Thanks again.
Posted: 21st Nov 2021 21:09
Cleaned up the code, turned things into functions. That's probably my final update on this.

Performance can be improved by include an additional array into the Point2D UDT to maintain the lengths of the segments. If your path isn't changing dynamically very often then I'd suggest that route. Although, I do think the square roots could be removed now that I look at it.