How do you work with Vectors?

I'm used to just using sin/cos math to calculate the distance from X,Y along an angle, but I'd rather use the proper API to both learn it, and I assume there's a performance benefit

I assume that means the vector API, but the API only has:

local vector = playdate.geometry.vector2D.new(X, Y)
vector :projectAlong(another vector)

how do I do vector.projectAlong(angle, distance) instead of another X,Y coordinate?

Also, I hope 0 is north/up, and it goes clockwise? Cause in some languages it starts as 0 = right/east counterclockwise and that's just annoying to visualize...

I translated my code from VB6

local geo <const>				= pd.geometry
local PI <const>				= 3.14159265358979
local PI180 <const>				= 180 / PI

function LineLineIntercept(X1, Y1, X2, Y2, X3, Y3, X4, Y4)
	local does, x, y 			= geo.lineSegment.fast_intersection(X1, Y1, X2, Y2, X3, Y3, X4, Y4)
	if does then
		return 					{x,y} --, X1, Y1, X2, Y2, X3, Y3, X4, Y4}
	end
	return 						false
end

function LineRectIntercept(X1, Y1, X2, Y2, X, Y, W, H)
	X1		 					= geo.lineSegment.new(X1, Y1, X2, Y2)
	X							= geo.rect.new(X, Y, W, H)
	local intersects, intersectionPoints = X1:intersectsRect(X)
	if intersects then
		return 					intersectionPoints
	end
	return						{}
end

function oldLineLineIntercept(X1, Y1, X2, Y2, X3, Y3, X4, Y4)
    local a1 					= Y2 - Y1
    local b1 					= X1 - X2
    local c1 					= X2 * Y1 - X1 * Y2

    local a2 					= Y4 - Y3
    local b2 					= X3 - X4
    local c2 					= X4 * Y3 - X3 * Y4

    local denom 				= a1 * b2 - a2 * b1

    if denom ~= 0 then
        return {
			(b1 * c2 - b2 * c1) / denom,
			(a2 * c1 - a1 * c2) / denom,
		}
	end
	return 						false
end

function Distance(X1, Y1, X2, Y2) -- CORRECT!!!
	X1							= geo.point.new(X1, Y1)
	X2							= geo.point.new(X2, Y2)
	return 						X1:distanceToPoint(X2)
end

function GetAngle(X1, Y1, X2, Y2)
	if X1 == X2 then -- going straight up or down
		if Y1 < Y2 then -- going down
			return 				180
		elseif Y1 > Y2 then -- going up
			return				0
		end
		return 					false -- no movement
	elseif Y1 == Y2 then -- going straight left or right
		if X1 < X2 then -- going right
			return 				90
		end--elseif X1 > X2 then -- going left
		return 				270
	end
	local Angle					= math.abs(RadToDeg(math.atan( Y1 - Y2 , X2 - X1 )))
	--local Before				= Angle
	--local Direction			= nil
	if X1 < X2 then -- going right (0 to 180)
		if Y1 < Y2 then -- going down right (90 to 180)
			--Direction			= "RIGHTDOWN"
			Angle				= 90 + Angle
		elseif Y1 > Y2 then -- going up right (0 to 90) actual: 90 to 0
			--Direction			= "RIGHTUP"
			Angle				= 90 - Angle
		end
	else --if X1 > X2 then going left (180 to 360)
		if Y1 < Y2 then -- going down left (180 to 270)
			--Direction			= "LEFTDOWN"
			Angle				= 90 + Angle
		elseif Y1 > Y2 then -- going up left (270 to 360)
			--Direction			= "LEFTUP"
			Angle				= 90 - Angle
		end
	end
	return 						 Limit(Angle, 360)
	--if Direction then
	--	print("Converted X1: " .. round(X1,0) .. " Y1: " .. round(Y1, 0) .. " - X2: " .. round(X2, 0) .. " Y2: " .. round(Y2, 0) .. " FROM: " .. Before .. " to " .. Angle .. " DIR: " .. Direction)
	--end
end

function findXY(X, Y, Distance, Angle, IsX, IsCorrected)
	if IsCorrected == nil or IsCorrected == false then
		Angle					= DegToRad(Angle)
	end
	if IsX == nil then -- return both as a table
		return 					{
			findXY(X, Y, Distance, Angle, true, true), -- 1=X
			findXY(X, Y, Distance, Angle, false, true), -- 2=Y
			Angle -- 3=Angle to Radians
		}
	elseif IsX then -- return the X
		return					X + math.sin(Angle) * Distance
	end -- return the Y
	return 						Y + math.cos(Angle) * Distance
end

function round(number, digits)
	digits 						= digits or 0
	return 						tonumber(string.format("%." .. digits .. "f", number))
end

function trigtest()
	local temp
	print("GET ANGLE OF 0,0 and 10,10: " .. GetAngle(0,0, 10,10)) -- should be 135, is 315 (+180)
	print("DISTANCE FROM 0,0 and 10,10: " .. Distance(0,0, 10,10)) -- 14.14214
	for angle = 0, 345, 15 do
		temp = findXY(0,0,10, angle)
		print(angle .. " degrees from 0,0 = X: " .. round(temp[1], 3) .. " Y: " .. round(temp[2], 3) .. " ANGLE: " .. round(GetAngle(0,0, temp[1], temp[2]), 0)) -- 8.939966
	end
	print("-----------------------------------------------------------------")
	-- oldLineLineIntercept
	temp 						= LineLineIntercept(0,0, 200,0,  100, -50, 100, 50  )
	printTable(temp)
	temp						= LineRectIntercept(0,0, 200, 0,  100, -50, 50, 50    )
	printTable(temp)
end

function Limit(Value, toWhat)
	while Value < 0 do
		Value 					= Value + toWhat
	end
	while Value >= toWhat do
		Value					= Value - toWhat
	end
	return 						Value
end

function DegToRad(Deg) -- 2 PI per full radian circle
	--            RADIANS                                 SHOULD BE
	--     		N = 0.78539							  N = 0 / 6.28 (E)
	-- W = 3.14   			E = 0 / 6.28	W=4.12389 (S)			E = 0.78539 (N)
	--     		S = 4.712389							  	S = 3.14 (W)
	--			  DEGREES								  CHANGE TO
	--		  	   N = 0									N = 90
	-- W = 270				E = 90               W = 180			E = 0 / 360
	--			  S = 180 								 	S = 270
	Deg 						= 180 - Deg
    Deg 						= Deg / 180 * PI
	return 						Deg
end
function RadToDeg(Rad)
    return						Rad * PI180
end


trigtest()

You're right that we don't currently have a built in method to construct a vector based on angle and magnitude, but perhaps that is something we should consider adding! As you noted, that would require us to pick a direction for 0°. I think you're right that north probably does make the most sense since 0° is up for the crank (even though it is normally east on the unit circle).

The implementation for that is not very complicated to implement in Lua and I've included it in the sample code below, which replicates the output of your trigTest() function using vectors. It also implements a visual version so you can see results as you move the crank. Overall I think the code is pretty simple, and of course it would get even simpler if we add the new vector constructor to the SDK.

import 'CoreLibs/graphics'

local pd <const> = playdate
local gfx <const>				= pd.graphics
local geo <const>				= pd.geometry

gfx.setDrawOffset(200, 120)
local northVector <const> = geo.vector2D.new(0, -1)  -- north-facing vector


local function newVector(magnitude, angle)
	angle = (angle + 270) % 360 -- rotate -90° so that 0° is north instead of east
	local rad = math.rad(angle)
	local x = magnitude * math.cos(rad)
	local y = magnitude * math.sin(rad)
	return geo.vector2D.new(x, y)
end


local function trigTest2()

	local magnitude = 10
	
	for angle = 0, 345, 15 do
		local v = newVector(magnitude, angle)
		local calculatedAngle = northVector:angleBetween(v)
		if calculatedAngle < 0 then calculatedAngle += 360 end -- positive angles only
		print(angle .. " degrees from 0,0 = X: " .. string.format("%.2f", v.x) .. " Y: " .. string.format("%.2f", v.y) .. " ANGLE: " .. string.format("%.0f", calculatedAngle))
	end
end


trigTest2()


function playdate.update()
	gfx.clear()
	gfx.setLineWidth(1)
	gfx.drawLine(0, -200, 0, 200)
	gfx.drawLine(-200, 0, 200, 0)
	
	local angle = playdate.getCrankPosition()
	
	local v = newVector(100, angle)
	gfx.drawText("x: " .. v.x .. "\ny: " .. v.y, -190, -110)
	gfx.setLineWidth(2)
	gfx.drawLine(0, 0, v.x, v.y)
	gfx.setLineWidth(1)
	gfx.fillCircleAtPoint(v.x, v.y, 3)
	
	local calculatedAngle = (northVector:angleBetween(v) + 360) % 360
	gfx.drawText("angle: " .. calculatedAngle, 10, 10)
end

Update: I'll get this added in a future SDK update.

4 Likes