Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

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

edited December 2014 in Questions Posts: 57

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.

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

Comments

  • dave1707dave1707 Mod
    edited November 2014 Posts: 10,053

    @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
  • edited November 2014 Posts: 57

    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.

  • dave1707dave1707 Mod
    Posts: 10,053

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

  • dave1707dave1707 Mod
    edited November 2014 Posts: 10,053

    @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!

  • edited November 2014 Posts: 342

    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!)

  • dave1707dave1707 Mod
    Posts: 10,053

    @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
    
  • edited November 2014 Posts: 342

    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
    
    
Sign In or Register to comment.