[RESOLVED] How to improve arc calculation for pie fractions drawing?

Hi, I wrote this program to teach my children about fractions. In some fractions, e.g. 1/13 or 1/15 the slices don’t quite fill the pie. Can someone suggest how to improve the arc calculation to fix this, please? Thank you.

http://www.youtube.com/watch?v=_uBVfcVVlfM

Main class

-- Fractions

function setup()
    Pick = 1
    Slices = 4
    parameter.integer("Pick", 1, 20, Pick, pick)
    parameter.integer("Slices", 1, 20, Slices, slice)
end

function draw()
    background(40, 40, 50)
    for _,p in pairs(pies) do
        p:draw()
    end
    if #pies > 1 then
        fontSize(40)
        fill(255, 255, 255, 255)
        text(Pick.."/"..Slices, WIDTH/2, HEIGHT-50)
    end
end

function slice(n)
    pick(Pick)
end

function pick(n)
    local reqPies = math.ceil(n / Slices)
    local cols = math.ceil(math.sqrt(reqPies))
    local rows = math.ceil(reqPies / cols)

    local radius = HEIGHT / cols / 4
    
    pies = {}
    for i = 1,reqPies do
        local col = (i - 1) % cols + 1
        local row = math.ceil(i / cols)
        
        local x = radius * col * 3 - radius
        local y = radius * row * 3 - radius
        
        local thisPick = Slices
        if i == reqPies then
            thisPick = Pick % Slices
            if thisPick == 0 then
                thisPick = Slices
            end
        end
        table.insert(pies, Pie(x, y, radius, Slices, thisPick))
    end
end

Arc class

Arc = class()

local delta = math.pi / 36

-- If the Arc is closed, radius will be drawn
function Arc:init(startAngle, endAngle, radius, closed)
    self.startAngle = startAngle
    self.endAngle = endAngle
    self.radius = radius
    if closed ~= nil then
        self.closed = closed
    else
        self.closed = false
    end
    self.points = {vec2(0, 0)}
    
    for angle = startAngle, endAngle, delta do
        local b = math.cos(angle) * radius
        local h = math.sin(angle) * radius
        table.insert(self.points, vec2(b, h))
    end
end

function Arc:draw()
    local n = #self.points
    
    local startPoint = 1
    local endPoint = n-2
    if self.closed then
        startPoint = 0
        endPoint = n-1
    end
    
    for i = startPoint, endPoint do
        local p1 = self.points[i%n + 1]
        local p2 = self.points[(i+1)%n + 1]
        line(p1.x, p1.y, p2.x, p2.y)
    end
end

Slice class

Slice = class()

function Slice:init(x, y, startAngle, endAngle, radius, colour)
    self.x = x
    self.y = y
    
    self.arc = Arc(startAngle, endAngle, radius, true)
    
    local vertices = {}
    local colours = {}
    for i = 2, (#self.arc.points-1) do
        table.insert(vertices, vec2(0, 0))
        table.insert(vertices, self.arc.points[i])
        table.insert(vertices, self.arc.points[i+1])
        
        table.insert(colours, colour)
        table.insert(colours, colour)
        table.insert(colours, colour)
    end
    
    self.mesh = mesh()
    self.mesh.vertices = vertices
    self.mesh.colors = colours
end

function Slice:draw()
    pushMatrix()
        translate(self.x, self.y)
        self.mesh:draw()
    popMatrix()
    
    strokeWidth(5)
    stroke(0, 0, 0, 255)
    self.arc:draw()
end

Pie class

Pie = class()

function Pie:init(x, y, radius, sliceCount, selected)
    self.x = x
    self.y = y
    self.radius = radius
    self.sliceCount = sliceCount
    self.selected = selected
    self.label = self.selected.."/"..self.sliceCount
    
    local delta = 2 * math.pi / self.sliceCount
    self.slices = {}
    local colour
    for i = 1,self.sliceCount do
        if i <= selected then
            colour = color(87, 64, 168, 255)
        else
            colour = color(127, 127, 127, 255)
        end
        table.insert(self.slices, Slice(0, 0, delta*(i-1), delta*i, radius, colour))
    end
end

function Pie:draw()
    pushMatrix()
        translate(self.x, self.y)
        for i,slice in ipairs(self.slices) do
            slice:draw()
        end
        fontSize(self.radius / 4)
        self.labelW, self.labelH = textSize(self.label)
        fill(255, 255, 255, 255)
        text(self.label, 0, self.radius + self.labelH)
    popMatrix()
end

@LightDye Here’s how I would do slices.

EDIT: Changed color and size of lines.


function setup()  
    parameter.integer("parts",1,25,4)  
    w=WIDTH/2
    h=HEIGHT/2
end

function draw()
    background(0)    
    fill(0, 198, 255, 255)
    noStroke()
    ellipse(w,h,405)
    stroke(0)
    strokeWidth(3)
    if parts>1 then
        for z=0,360,360/parts do
            x=math.cos(math.rad(z))*200
            y=math.sin(math.rad(z))*200
            line(w,h,w+x,h+y)
        end
    end
end

Thanks @dave1707, that works well when all slices are of the same colour, but I need to draw parts of the circle with different colour and that’s why I’m using mesh to build slices out of narrow triangles.

I can improve the fit of slices by drawing narrower triangles in the mesh just by reducing the delta variable in the Arc class:

local delta = math.pi / 72

But then I start noticing how the drawing slows down.

OK, I didn’t think about different colors for different slices.

@LightDye Is this better.

EDIT: Changed the code to make the lines thicker.


function setup()  
    tab={}
    m=mesh()
    fontSize(50)
    parameter.integer("frac",0,25,2)
    parameter.integer("parts",1,25,4)  
    w=WIDTH/2
    h=HEIGHT/2
end

function draw()
    background(0, 255, 224, 255)    
    if frac>parts then
        frac=parts
    end
    fill(0, 198, 255, 255)
    ellipse(w,h,408)
    text(frac.."/"..parts,w,h+250)    
    tab={}
    for z=1,360*frac/parts do
        table.insert(tab,vec2(w,h))        
        x=math.cos(math.rad(z-1))*198
        y=math.sin(math.rad(z-1))*198
        table.insert(tab,vec2(w+x,h+y))        
        x=math.cos(math.rad(z))*198
        y=math.sin(math.rad(z))*198
        table.insert(tab,vec2(w+x,h+y))
    end       
    m.vertices=tab
    m:setColors(255,0,0,255)
    m:draw()
    stroke(0)
    strokeWidth(5)
    if parts>1 then
        for z=0,360,360/parts do
            x=math.cos(math.rad(z))*200
            y=math.sin(math.rad(z))*200
            line(w,h,w+x,h+y)
        end
    end
end

Thanks @dave1707, that certainly looks better. Cheers!

If you replace the ‘text()’ line in @Dave1707’s code with this:

    for i = math.min(frac,parts),2,-1 do
    if parts%i==0 and frac%i==0 then
        text(frac/i.."/"..parts/(i),w,h+300)
        break
    end
    end
    text(frac.."/"..parts,w,h+250)

It will reduce the fractions nicely (good for the kiddos!)

@Monkeyman32123 You’re doing extra divisions in the % line of code

if (parts/i)%1==0 and (frac/i)%1==0 then

can be changed to

if parts%i==0 and frac%i==0 then

Ah, yes, I see that now, how silly of me >_<

Thank you for pointing that out (didn’t really go over it very well, just whipped it up)

Edited in the code above.

Nice addition to the code @Monkeyman32123, thank you!

You’re welcome, any time!

Codea Fractions

Turns out that what makes my code slow is the drawing of line segments that form each slice border. After adapting @dave1707’s code to my code and adding an FPS class this became obvious. I’m sharing the improved code here for future reference.

Main class

-- Fractions

function setup()
    footerH = 50
    Pick = 1
    Slices = 4
    parameter.integer("Pick", 1, 40, Pick, pick)
    parameter.integer("Slices", 1, 40, Slices, slice)
    parameter.boolean("ShowFPS", false)
    parameter.boolean("OOdrawing", false, OOdrawingChanged)
end

function draw()
    background(40, 40, 50)
    for _,p in pairs(pies) do
        p:draw()
    end
    if #pies > 1 then
        fontSize(40)
        fill(255, 232, 0, 255)
        text(fraction.label, WIDTH/2, footerH/2)
    end
    if ShowFPS then
        fps:draw()
    end
end

function slice(n)
    pick(Pick)
end

function pick(n)
    fps = FPS()
    
    fraction = Fraction(Pick, Slices)
    
    local reqPies = math.ceil(n / Slices)
    local cols = math.ceil(math.sqrt(reqPies))
    local rows = math.ceil(reqPies / cols)

    local radius = HEIGHT / cols / 3
    local colW = WIDTH / cols
    local rowH = (HEIGHT - footerH) / rows
    
    pies = {}
    for i = 1,reqPies do
        local col = (i - 1) % cols + 1
        local row = math.ceil(i / cols)
        
        local x = col * colW - colW/2
        local y = row * rowH - rowH/2 + footerH
        
        local thisPick = Slices
        if i == reqPies then
            thisPick = Pick % Slices
            if thisPick == 0 then
                thisPick = Slices
            end
        end
        table.insert(pies, Pie(x, y, radius, Slices, thisPick))
    end
end

function OOdrawingChanged()
    fps = FPS()
end

FPS class

FPS = class()

function FPS:init()
    self.min = 1000
    self.max = 0
    self.rate = 60
    self.count = 0
    self.total = 0
end

function FPS:calc()
    self.count = self.count + 1
    self.rate = math.floor(1 / DeltaTime)
    self.min = math.min(self.min, self.rate)
    self.max = math.max(self.max, self.rate)
    self.total = self.total + self.rate
    self.average = math.floor(self.total / self.count) 
end

function FPS:draw()
    self:calc()
    pushStyle()
        fontSize(20)
        noStroke()
        rectMode(CORNER)
        textMode(CORNER)
        pushMatrix()
            local board = self.rate..", "..self.min..".."..self.average..".."..self.max
            local w,h = textSize(board)
            fill(0, 0, 0, 255)
            rect(WIDTH-w, HEIGHT-h, WIDTH, HEIGHT)
            fill(255, 0, 0, 255)
            text(board, WIDTH-w, HEIGHT-h)
        popMatrix()
    popStyle()
end

Fraction class

Fraction = class()

function Fraction:init(numerator, denominator)
    self.numerator = numerator
    self.denominator = denominator
    self.label = self.numerator.."/"..self.denominator
    
    local n,d = self:reduce()
    if d < self.denominator then
        self.label = self.label.." = "..n.."/"..d
    end
end

function Fraction:reduce()
    -- Based on code by Monkeyman32123
    for i = math.min(self.numerator, self.denominator),1,-1 do
        if self.numerator%i==0 and self.denominator%i==0 then
            return self.numerator/i, self.denominator/i
        end
    end
end

Arc class

Arc = class()

local delta = math.pi / 180

-- If the Arc is closed, radius will be drawn
function Arc:init(startAngle, endAngle, radius, closed)
    self.startAngle = math.min(startAngle, endAngle)
    self.endAngle = math.max(startAngle, endAngle)
    self.radius = radius
    if closed ~= nil then
        self.closed = closed
    else
        self.closed = false
    end
    self.points = {vec2(0, 0)}
    
    local n = (self.endAngle - self.startAngle) / delta
    for i = 0, n-1 do
        local angle = self.startAngle + delta * i
        self:addPoint(angle)
    end
    self:addPoint(self.endAngle)
end

function Arc:addPoint(angle)
    local b = math.cos(angle) * self.radius
    local h = math.sin(angle) * self.radius
    table.insert(self.points, vec2(b, h))
end

-- Very FPS-costly function
function Arc:draw()
    local n = #self.points
    
    local startPoint = 1
    local endPoint = n-2
    if self.closed then
        startPoint = 0
        endPoint = n-1
    end
    
    for i = startPoint, endPoint do
        local p1 = self.points[i%n + 1]
        local p2 = self.points[(i+1)%n + 1]
        line(p1.x, p1.y, p2.x, p2.y)
    end
end

Slice class

Slice = class()

function Slice:init(x, y, startAngle, endAngle, radius, colour)
    self.x = x
    self.y = y
    
    self.arc = Arc(startAngle, endAngle, radius, true)
    
    local vertices = {}
    local colours = {}
    for i = 2, (#self.arc.points-1) do
        table.insert(vertices, vec2(0, 0))
        table.insert(vertices, self.arc.points[i])
        table.insert(vertices, self.arc.points[i+1])
        
        table.insert(colours, colour)
        table.insert(colours, colour)
        table.insert(colours, colour)
    end
    
    self.mesh = mesh()
    self.mesh.vertices = vertices
    self.mesh.colors = colours
end

function Slice:draw()
    pushMatrix()
        translate(self.x, self.y)
        self.mesh:draw()
    popMatrix()
    
    if OOdrawing then
    -- This has performance issues
        strokeWidth(4)
        stroke(0, 0, 0, 255)
        self.arc:draw()
    end
end

Pie class

Pie = class()

function Pie:init(x, y, radius, sliceCount, selected)
    self.x = x
    self.y = y
    self.radius = radius
    self.sliceCount = sliceCount
    self.selected = selected
    
    self.fraction = Fraction(selected, sliceCount)
     
    local delta = 2 * math.pi / self.sliceCount
    self.slices = {}
    local colour
    for i = 1,self.sliceCount do
        if i <= selected then
            colour = color(60, 132, 64, 255)
        else
            colour = color(127, 127, 127, 255)
        end
        table.insert(self.slices, Slice(0, 0, delta*(i-1), delta*i, radius, colour))
    end
end

function Pie:draw()
    pushMatrix()
        translate(self.x, self.y)
        for i,slice in ipairs(self.slices) do
            slice:draw()
        end
    
        if not OOdrawing then
            -- Draws the circle to solve the performance issue of arc drawing
            noFill()
            ellipseMode(RADIUS)
            ellipse(0, 0, self.radius + strokeWidth())
            -- Draws all radius here instead of in Arc for performance reasons
            stroke(255, 255, 255, 255)
            strokeWidth(2)
            smooth()
            for _,slice in ipairs(self.slices) do
                local a = slice.arc.startAngle
                local r = slice.arc.radius
                local x = math.cos(a) * r
                local y = math.sin(a) * r
                line(0, 0, x, y)
            end
        end
    
        fontSize(self.radius / 4)
        self.labelW, self.labelH = textSize(self.fraction.label)
        fill(255, 255, 255, 255)
        text(self.fraction.label, 0, self.radius + self.labelH)
    popMatrix()
end

There are better ways to draw an arc than as a sequence of lines. Here’s my arc code. It’s long because it is flexible. It uses a mesh to draw the arc. There’s an arc function and an Arc class depending on how one wants to invoke it.

There’s also an arc shader in Codea; I don’t think it is all that fast.

-- Arc path drawing

Arc = class()

local __makeArc = function(nsteps)
    -- nsteps doesn't make a huge difference in the range 50,300
    nsteps = nsteps or 50
    local m = mesh()
    m.shader = shader([[
//
// A basic vertex shader
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;
uniform lowp vec4 scolour;
uniform lowp vec4 ecolour;
lowp vec4 mcolour = ecolour - scolour;
//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying highp vec2 vTexCoord;
varying lowp vec4 vColour;
varying highp float vWidth;
varying highp float vCore;

uniform float width;
uniform float taper;
uniform float blur;
uniform float cap;
uniform float scap;
uniform float ecap;
float swidth = width + blur;
float ewidth = taper*width - width;
float ecapsw = clamp(cap,0.,1.)*ecap;
float scapsw = clamp(cap,0.,1.)*scap;
uniform vec2 centre;
uniform vec2 xaxis;
uniform vec2 yaxis;
uniform float startAngle;
uniform float deltaAngle;

void main()
{
    highp float t = clamp(position.y,0.,1.);
    vCore = t;
    highp float w = smoothstep(0.,1.,t);
    vWidth = w*ewidth + swidth;
    highp vec2 bpos = centre + cos(t*deltaAngle + startAngle) * xaxis + sin(t*deltaAngle + startAngle) * yaxis;
    highp vec2 bdir = -sin(t*deltaAngle + startAngle) * xaxis + cos(t*deltaAngle + startAngle) * yaxis;
    bdir = vec2(bdir.y,-bdir.x);
    bdir = vWidth*normalize(bdir);
    bpos = bpos + position.x*bdir;
    highp vec4 bzpos = vec4(bpos.x,bpos.y,0.,1.);
    bzpos.xy += (ecapsw*max(position.y-1.,0.)
                +scapsw*min(position.y,0.))*vec2(-bdir.y,bdir.x);
    highp float s = clamp(position.y, 
            scapsw*position.y,1.+ecapsw*(position.y-1.));
    vTexCoord = vec2(texCoord.x,s);
    vColour = t*mcolour + scolour;
    //Multiply the vertex position by our combined transform
    gl_Position = modelViewProjection * bzpos;
}
]],[[
//
// A basic fragment shader
//

uniform highp float blur;
uniform highp float cap;

varying highp vec2 vTexCoord;
varying highp float vWidth;
varying lowp vec4 vColour;
varying highp float vCore;

void main()
{
    lowp vec4 col = vColour;
    highp float edge = blur/(vWidth+blur);
    col.a = mix( 0., col.a, 
            (2.-cap)*smoothstep( 0., edge, 
                min(vTexCoord.x,1. - vTexCoord.x) )
            * smoothstep( 0., edge, 
                min(1.5-vTexCoord.y, .5+vTexCoord.y) ) 
            + (cap - 1.)*smoothstep( 0., edge,
             .5-length(vTexCoord - vec2(.5,vCore)))
                );

    gl_FragColor = col;
}
]])

    for n=1,nsteps do
        m:addRect(0,(n-.5)/nsteps,1,1/nsteps)
    end
    m:addRect(0,1.25,1,.5)
    m:addRect(0,-.25,1,.5)
    return m
end

local m = __makeArc()
m.shader.blur = 2
m.shader.cap = 2
m.shader.scap = 1
m.shader.ecap = 1

-- centre, xaxis, yaxis, startAngle, deltaAngle, taper
function arc(a,b,c,d,e,f)
    if type(a) == "table" then
        f = b
        a,b,c,d,e = unpack(a)
    end
    if type(b) ~= "userdata" then
        b = b*vec2(1,0)
    end
    if type(c) ~= "userdata" then
        c = c*vec2(0,1)
    end
    --m.shader.blur = 15
    m.shader.taper = f or 1
    m.shader.width = strokeWidth()
    m.shader.scolour = color(stroke())
    m.shader.ecolour = color(stroke())
    m.shader.cap = (lineCapMode()-1)%3
    m.shader.centre = a
    m.shader.xaxis = b
    m.shader.yaxis = c
    m.shader.startAngle = d
    m.shader.deltaAngle = e
    m:draw()
end

function Arc:init(...)
    self:setParams(...)
end

function Arc:clone()
    return Arc(self.params)
end

function Arc:makeDrawable(t)
    t = t or {}
    local nsteps = t.steps or self.steps
    local m = __makeArc(nsteps)
    m.shader.taper = t.taper or self.taper or 1
    m.shader.blur = t.blur or self.blur or 2
    m.shader.cap = t.cap or self.cap or (lineCapMode()-1)%3
    m.shader.scap = t.scap or self.scap or 1
    m.shader.ecap = t.ecap or self.ecap or 1
    m.shader.width = t.width or self.width or strokeWidth()
    m.shader.scolour = t.scolour or self.scolour or t.colour or color(stroke())
    m.shader.ecolour = t.ecolour or self.ecolour or t.colour or color(stroke())
    local a,b,c,d,e = unpack(self.params)
    m.shader.centre = a
    m.shader.xaxis = b
    m.shader.yaxis = c
    m.shader.startAngle = d
    m.shader.deltaAngle = e
    self.curve = m
    self.draw = function(self) self.curve:draw() end
end

function Arc:draw(t)
    self:makeDrawable(t)
    self.curve:draw()
end

function Arc:setParams(a,b,c,d,e)
    if type(a) == "table" then
        a,b,c,d,e = unpack(a)
    end
    if type(b) ~= "userdata" then
        b = b*vec2(1,0)
    end
    if type(c) ~= "userdata" then
        c = c*vec2(0,1)
    end
    self.params = {a,b,c,d,e}
    if self.curve then
        self.curve.shader.centre = a
        self.curve.shader.xaxis = b
        self.curve.shader.yaxis = c
        self.curve.shader.startAngle = d
        self.curve.shader.deltaAngle = e
    end
end

function Arc:setStyle(t)
    self.scolour = t.scolour or t.colour or self.scolour
    self.ecolour = t.ecolour or t.colour or self.ecolour
    self.width = t.width or self.width
    self.taper = t.taper or self.taper
    self.blur = t.blur or self.blur
    self.cap = t.cap or self.cap
    self.scap = t.scap or self.scap
    self.ecap = t.ecap or self.ecap
    if not self.curve then 
        return
    end
    t = t or {}
    if t.colour then
        self.curve.shader.scolour = t.colour
        self.curve.shader.ecolour = t.colour
    end
    if t.scolour then
        self.curve.shader.scolour = t.scolour
    end
    if t.ecolour then
        self.curve.shader.ecolour = t.ecolour
    end
    if t.width then
        self.curve.shader.width = t.width
    end
    if t.taper then
        self.curve.shader.taper = t.taper
    end
    if t.blur then
        self.curve.shader.blur = t.blur
    end
    if t.cap then
        self.curve.shader.cap = t.cap
    end
    if t.scap then
        self.curve.shader.scap = t.scap
    end
    if t.ecap then
        self.curve.shader.ecap = t.ecap
    end
end

function Arc:point(t)
    local a,b,c,d,e = unpack(self.params)
    return a + math.cos(t*e + d)*b + math.sin(t*e + d)*c
end

function Arc:tangent(t)
    local a,b,c,d,e = unpack(self.params)
    return -math.sin(t*e + d)*b + math.cos(t*e + d)*c
end

function Arc:normal(t)
    return self:tangent(t):rotate90()
end

function Arc:unitNormal(t)
    local pt = self:normal(t)
    local l = pt:len()
    if l == 0 then
        return vec2(0,0)
    else
        return pt/l
    end
end

function Arc:unitTangent(t)
    local pt = self:tangent(t)
    local l = pt:len()
    if l == 0 then
        return vec2(0,0)
    else
        return pt/l
    end
end