How to Draw an Arc on Dialog Canvas v1.3-rc1

Hi all,

I imagine an arc might be a useful figure for a dialog canvas in the future. Here is some tinkering to draw it with the new graphics context feature in version 1.3-rc1. This isn’t intended to be comprehensive on all the details. It’s a quick experiment to make the search for a how-to shorter and easier.

canvasArcScreencap

local function arc(ctx, startAngle, stopAngle, radius, xc, yc)
    local cos = math.cos
    local sin = math.sin
    local tau = math.pi + math.pi
    local halfpi = math.pi * 0.5

    local stAngVerif = math.min(startAngle, stopAngle)
    local edAngVerif = math.max(startAngle, stopAngle)
    local arcLength = math.min(edAngVerif - stAngVerif, tau)

    local arcLen01 = arcLength / tau
    local knCtVerif = math.ceil(1 + 4 * arcLen01)
    local toStep = 1.0 / (knCtVerif - 1.0)
    local invKnCt = toStep * arcLen01
    local handleMag = radius * (4.0 / 3.0) * math.tan(halfpi * invKnCt)

    local cosAngle = cos(-stAngVerif)
    local sinAngle = sin(-stAngVerif)
    local xap = xc + radius * cosAngle
    local yap = yc + radius * sinAngle
    local hmsina = sinAngle * handleMag
    local hmcosa = cosAngle * handleMag
    local cp1x = xap + hmsina
    local cp1y = yap - hmcosa
    local cp2x = 0
    local cp2y = 0

    ctx:beginPath()
    ctx:moveTo(xc, yc)
    ctx:lineTo(xap, yap)

    local i = 1
    while i < knCtVerif do
        local t = i * toStep
        local u = 1.0 - t
        local angle = u * stAngVerif + t * edAngVerif

        cosAngle = cos(-angle)
        sinAngle = sin(-angle)
        xap = xc + radius * cosAngle
        yap = yc + radius * sinAngle

        hmsina = sinAngle * handleMag
        hmcosa = cosAngle * handleMag
        cp2x = xap - hmsina
        cp2y = yap + hmcosa

        ctx:cubicTo(cp1x, cp1y, cp2x, cp2y, xap, yap)

        cp1x = xap + hmsina
        cp1y = yap - hmcosa

        i = i + 1
    end

    ctx:lineTo(xc, yc)
    ctx:closePath()
end

local dlg = Dialog { title = "Canvas Arc" }

dlg:canvas {
    width = 256,
    height = 256,
    onpaint = function(ev)
        local args = dlg.data
        local startDeg = args.startDeg --[[@as integer]]
        local stopDeg = args.stopDeg --[[@as integer]]

        local startRad = startDeg * (math.pi / 180.0)
        local stopRad = stopDeg * (math.pi / 180.0)

        local ctx = ev.context

        local xc = ctx.width * 0.5
        local yc = ctx.height * 0.5
        local w = 64
        local h = w

        ctx.antialias = true

        ctx.strokeWidth = 2.0
        ctx.color = Color { r = 0, g = 127, b = 255 }
        arc(ctx, startRad, stopRad, w, xc, yc)
        ctx:stroke()
    end
}

dlg:slider {
    id = "startDeg",
    label = "Start:",
    min = 0,
    max = 360,
    value = 90,
    onchange = function()
        dlg:repaint()
    end
}

dlg:slider {
    id = "stopDeg",
    label = "Stop:",
    min = 0,
    max = 360,
    value = 180,
    onchange = function()
        dlg:repaint()
    end
}

dlg:show { wait = false }

Variations will depend on whether you want an arc to sweep the shortest path, longest path, etc. You might also expect the start and stop angles to be in [0, 359] or to exceed that range. There is also the question of whether a positive angle is clockwise or counter-clockwise. In terms of appearance, I imagine you could create a stroke only, a chord, a pie slice as above, or a sector with two arcs of different radii connected by a flat end cap. You could also return early with a circle if the start angle and stop angle are 360 degrees apart.

For complex curves, I find it useful to draw diagnostic lines of the control points; or to even create objects for piecewise curves.

There are a lot of primers on the maths of Bezier curves (cubic and otherwise) out there. Much exceeds my own maths understanding. One I’ve found reliable, though, is from pomax (Mike Kamermans):

Cheers,
Jeremy

5 Likes