How’s this?

```
function setup()
--displayMode(FULLSCREEN)
canvas = image(WIDTH,HEIGHT)
setContext(canvas)
background(147, 177, 180, 255)
setContext()
pen = vec2(WIDTH/2, HEIGHT/2)
end
function draw()
background(255, 0, 0, 255)
noSmooth()
if joystick then
setContext(canvas)
blendMode(ZERO, ONE, ZERO, ONE_MINUS_SRC_ALPHA)
fill(0, 0, 0, 200)
noStroke()
pen = pen + vec2(joystick.dx, joystick.dy)
ellipse(pen.x, pen.y, 2)
canvas.premultiplied = false
blendMode(NORMAL)
setContext()
end
spriteMode(CORNER)
sprite(canvas, 0,0)
if joystick then
--show HUD
pushStyle()
fill(216, 216, 216, 255)
ellipse(pen.x, pen.y, 2)
noFill()
stroke(30, 99, 122, 255)
strokeWidth(1)
ellipse(joystick.cx, joystick.cy, 120)
strokeWidth(2)
line(joystick.cx, joystick.cy, joystick.x, joystick.y)
popStyle()
end
end
-- Map a number to a new range of numbers
-- example: map(255, 0,255, 0,1) would map 255 to 1 in a range of [0-1]
local function map(value, start1, stop1, start2, stop2)
local n = type(value) == "table" and value or {value}
local t = {}
for i = 1, #n do
local norm = (n[i] - start1) / (stop1 - start1)
norm = norm * (stop2 - start2) + start2
table.insert(t, norm)
end
return type(value) == "table" and t or unpack(t)
end
function touched(touch)
if touch.state == BEGAN then
if not joystick then
joystick = {
id = touch.id,
cx = touch.x,
cy = touch.y,
x = touch.x,
y = touch.y,
dx = 0,
dy = 0
}
end
end
if touch.state == ENDED or touch.state == CANCELLED then
if joystick and joystick.id == touch.id then
joystick = nil
end
end
if touch.state == MOVING then
if joystick and joystick.id == touch.id then
local dx, dy = joystick.x - joystick.cx, joystick.y - joystick.cy
local pos = {0, 0}
if vec2(joystick.cx, joystick.cy):dist(vec2(joystick.x, joystick.y)) >= 150 then
local rad = -(math.floor(math.atan2(dy, dx) / math.pi * 2 + 0.5) / 2 * math.pi) + math.pi / 2
local sin, cos = math.sin(rad), math.cos(rad)
sin, cos = math.abs(touch.x - joystick.cx) * sin, math.abs(touch.y - joystick.cy) * cos
pos[1], pos[2] = sin, cos
elseif vec2(joystick.cx, joystick.cy):dist(vec2(joystick.x, joystick.y)) >= 125 then
local rad = -(math.floor(math.atan2(dy, dx) / math.pi * 4 + 0.5) / 4 * math.pi) + math.pi / 2
local sin, cos = math.sin(rad), math.cos(rad)
sin, cos = math.abs(touch.x - joystick.cx) * sin, math.abs(touch.y - joystick.cy) * cos
pos[1], pos[2] = sin, cos
elseif vec2(joystick.cx, joystick.cy):dist(vec2(joystick.x, joystick.y)) >= 100 then
local rad = -(math.floor(math.atan2(dy, dx) / math.pi * 8 + 0.5) / 8 * math.pi) + math.pi / 2
local sin, cos = math.sin(rad), math.cos(rad)
sin, cos = math.abs(touch.x - joystick.cx) * sin, math.abs(touch.y - joystick.cy) * cos
pos[1], pos[2] = sin, cos
elseif vec2(joystick.cx, joystick.cy):dist(vec2(joystick.x, joystick.y)) >= 75 then
local rad = -(math.floor(math.atan2(dy, dx) / math.pi * 16 + 0.5) / 16 * math.pi) + math.pi / 2
local sin, cos = math.sin(rad), math.cos(rad)
sin, cos = math.abs(touch.x - joystick.cx) * sin, math.abs(touch.y - joystick.cy) * cos
pos[1], pos[2] = sin, cos
else
pos[1], pos[2] = touch.x - joystick.cx, touch.y - joystick.cy
end
joystick.x, joystick.y = touch.x, touch.y
joystick.dx, joystick.dy = unpack(map(pos, -1,1, -.004,.004))
end
end
end
```

The precision increases pretty quickly, you can change it how you want. Only edit is at the bottom, in touch.state == MOVING.

Of course, to my mind, the simplest answer is trigonometry. (Though, it does work pretty well…)