Help with Visual Timer

I’m trying to creat a timer that looks like a circle with the center punched out. As the counter ticks down I’d like the circle to diminish in size. Example full timer would be a 360 degree circle half would be 180. I have an idea I’m working on but my FPS are terrible. The idea is creating a circle using Sin and Cos and for every degree I draw a line. Is there a better more efficient way of doing this?


--# Main
function setup()
    displayMode(FULLSCREEN)
    t = TimerButton({x = x,y = y,size = 150,counterWidth = 15,angleSize = .5})
    b = TimerButton()

end


function draw()
    background(127, 127, 127, 255)
    t:draw()
    b:draw()
    
    
end

--# TimerButton
TimerButton = class()

function TimerButton:init(tbl)
    -- you can accept and set parameters here
    if not tbl then tbl = {} end
    self.x = tbl.x or WIDTH/2
    self.y = tbl.y or HEIGHT/2
    self.startColor = tbl.startColor or color(26, 255, 0, 255)
    self.midColor = tbl.midColor or color(254, 255, 0, 255)
    self.endColor = tbl.endColor or color(255, 0, 0, 255)
    self.x1 = 0
    self.y1 = 0
    self.size = tbl.size or 100
    self.counterWidth = tbl.counterWidth or 25
    self.angle = 0 
    self.angleSize = tbl.angleSize or 0.5
    self.loc = {}
    
    while self.angle < 360 do
        local t = {}
        t.x = self.size * math.cos(math.rad(self.angle))
        t.y = self.size * math.sin(math.rad(self.angle))
        t.x1 = t.x +(self.counterWidth * math.cos(math.rad(self.angle)))
        t.y1 = t.y + (self.counterWidth * math.sin(math.rad(self.angle)))

        self.angle = self.angle + self.angleSize
        table.insert(self.loc,t)
    end
end

function TimerButton:draw()
    -- Codea does not automatically call this method
    pushMatrix()
    translate(self.x,self.y)
    stroke(self.startColor)
    strokeWidth(4)
    for i = 1,#self.loc do
        
        line(self.loc[i].x,self.loc[i].y,self.loc[i].x1,self.loc[i].y1)
    end
    popMatrix()
end

function TimerButton:touched(touch)
    -- Codea does not automatically call this method
end

@Chipis Something like this.


function setup()
    parameter.integer("time",1,60,30)
    st=false
    a=0
    str="Tap screen to start timer"
end

function draw()
    background(40,40,50)
    stroke(255)
    strokeWidth(2)
    for z=a,360,.2 do
        x=math.sin(math.rad(z))*WIDTH/2
        y=math.cos(math.rad(z))*WIDTH/2
        line(WIDTH/2,HEIGHT/2,WIDTH/2+x,HEIGHT/2+y)
    end
    fill(255, 0, 0, 255)
    text(str,WIDTH/2,HEIGHT/2)
    if st then
        t1=os.date("*t")
        sec=t1.sec
        if t2~=sec then
            t2=sec
            a=a+360/time
        end
        if a>=360 then
            st=false
            str="Time expired"
        end
    end
end

function touched(t)
    if t.state==BEGAN then
        st=true
        str=""
        t1=os.date("*t")
        t2=t1.sec
    end
end

@Chipis I changed the above code to use a time set with the parameter slider. Select the number of seconds then tap the screen.

EDIT: Changed the above program again. Added os.date to the touched routine so the timer didn’t jump when it started.

Thanks @dave1707 but I’m still getting about 5 fps. Same problem I was having with my test. I feel its the fact that we are drawing to many lines each draw.

What i’m trying to accomplish is similar to the demo arc shader, but I can figure out how to do it without using a shader.

@Chipis I didn’t look at the FPS, so I didn’t realize it was so low. I guess the way I was doing it doesn’t work that well. I’ll have to try something else.

Here’s something I made a little while ago for my game. The color tweens from green to red as time passes and it beeps more and more frequently, but those thigs could easily be removed, just like size changing could easily be added. Here it is:


--# Main
-- Timer

-- Use this function to perform your initial setup
function setup()
    parameter.watch("1/DeltaTime")
    t = Timer(15)
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(255, 255, 255, 255)

    -- This sets the line thickness
    strokeWidth(5)

    -- Do your drawing here
    t:draw()
end

function touched(touch)
    if touch.state == ENDED then
        if touch.tapCount == 1 then
            if t.timing == nil then
                t:start()
            elseif not t.paused then
                t:pause()
            else
                t:resume()
            end
        else
            t:restart()
        end
    end
end

--# Timer
Timer = class()

function Timer:init(t, cb, x, y, s)
    self.time = t or 5
    self.callback = cb or function() end
    self.x = x or WIDTH / 2
    self.y = y or HEIGHT / 2
    self.size = s or HEIGHT * 3/4
    
    self.timing = nil
    self.beeping = nil
    self.amnt = 0
    self.beepTime = 10
    
    self.paused = false
    
    self.tMesh = mesh()
    self.tMesh.vertices = triangulate({vec2(-self.size / 2, -self.size / 2),
                        vec2(-self.size / 2, self.size / 2),
                        vec2(self.size / 2, self.size / 2),
                        vec2(self.size / 2, -self.size / 2)})
    self.tMesh.shader = shader("Patterns:Arc")
    self.tMesh.shader.a1 = math.pi
    self.tMesh.shader.a2 = math.pi
    self.tMesh.shader.size = .45
    self.tMesh.shader.color = vec4(0, 1, 0, 1)
    self.tMesh.texCoords = triangulate({vec2(0,0),vec2(0,1),vec2(1,1),vec2(1,0)})
end

function Timer:beep()
    sound("Game Sounds One:Menu Select")
    
    self.beepTime = self.beepTime + 2
    
    self.beeping = tween.delay(self.time / self.beepTime, function() self:beep() end)
end

function Timer:start()
    self.amnt = 0
    self.beepTime = 10
    if self.timing == nil then
        self.timing = tween(self.time, self, { amnt = 1 }, tween.easing.linear, self.callback)
    end
    if self.beeping == nil then
        self.beeping = tween.delay(self.time / self.beepTime, function() self:beep() end)
    end
end

function Timer:pause()
    if self.timing ~= nil then
        tween.stop(self.timing)
    end
    if self.beeping ~= nil then
        tween.stop(self.beeping)
    end
    
    self.paused = true
end

function Timer:resume()
    if self.timing ~= nil then
        tween.play(self.timing)
    end
    if self.beeping ~= nil then
        tween.stop(self.beeping)
    end
    
    self.paused = false
end

function Timer:stop()
    if self.timing ~= nil then
        tween.stop(self.timing)
        self.timing = nil
    end
    if self.beeping ~= nil then
        tween.stop(self.beeping)
        self.beeping = nil
    end
end

function Timer:restart()
    self:stop()
    self:start()
end

function Timer:draw()
    -- Update timer
    self.tMesh.shader.color = vec4(1 * self.amnt, 1 - (1 * self.amnt), 0, 1)
    self.tMesh.shader.a2 = -self.amnt * (math.pi * 2) + math.pi
    
    -- Draw timer
    pushMatrix()
    
    translate(self.x, self.y)
    local t
    if self.timing then t = math.ceil(self.timing.time - self.timing.running) else t = self.time end
    fontSize(self.size*2/3) fill(255 * self.amnt, 255 - (255 * self.amnt), 0, 255)
    -- text(t)
    
    rotate(270.1)
    
    self.tMesh:draw()
    
    popMatrix()
end

function Timer:touched(touch)
    -- Codea does not automatically call this method
end

@Chipis Here’s another version that has a high FPS.


function setup()
    parameter.integer("time",1,60,30)
    str="Tap screen to start timer"
    a=0
    st=false
    xx()    
end

function xx()
    tab={}
    for z=a,360 do
        x=math.sin(math.rad(z))*WIDTH/2
        y=math.cos(math.rad(z))*WIDTH/2
        if z>a then
            table.insert(tab,vec2(WIDTH/2,HEIGHT/2))
            table.insert(tab,vec2(WIDTH/2+x,HEIGHT/2+y))
            table.insert(tab,vec2(WIDTH/2+x1,HEIGHT/2+y1))
        end
        x1=x
        y1=y
    end
    m=mesh()
    m.vertices=tab
    m:setColors(255,0,0)
end

function draw()
    background(40, 40, 50)
    m.draw(m)
    t1=os.date("*t")
    if st then
        sec=t1.sec
        if t2~=sec then
            t2=sec
            a=a+360/time
            xx()
            if a>=360 then
                str="Time expired"
            end
        end
    end
    fill(40,40,50)
    ellipse(WIDTH/2,HEIGHT/2,WIDTH-50)
    fill(255)
    text(string.format("%d",1/DeltaTime),WIDTH/2,HEIGHT-50)
    text(str,WIDTH/2,HEIGHT/2)    
end

function touched(t)
    if t.state==BEGAN then
        st=true
        str=""
        xx()
        t1=os.date("*t")
        t2=t1.sec
    end
end

Here is my attempt with the arc shader. I can not figure out how to correctly increment it smoothly or accurately. Maybe someone brighter then I could fix it. It is working great in concept and it is fast.


--# Main
--Visual Timer

supportedOrientations(LANDSCAPE_ANY)
function setup()

    t=Timer(20)
    t1 = Timer(5,100,100,{width=5,radius=50})
    t2 = Timer(10,WIDTH-100,HEIGHT-100,{width=10,radius=50})
    t:start()
    t1:start()
    t2:start()
    parameter.watch("time")
    
    

end

function draw()
    background(255, 255, 255, 255)
    time = t.time
    t:draw()
    t1:draw()
    t2:draw()
--arc(cx, cy, radius, startAngle, endAngle)
end

function touched(t)
    
end


--# Shader
Shader = {}

Shader.vertex = [[
//
// Vertex shader: Arc
//

uniform mat4 modelViewProjection;

attribute vec4 position;
attribute vec2 texCoord;

varying highp vec2 vTexCoord;

void main() {
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}

]]

Shader.fragment = [[
//
// Fragment shader: Arc
//

precision highp float;

uniform float size;
uniform float a1;
uniform float a2;
uniform vec4 color;

varying vec2 vTexCoord;

void main() {
    vec4 col = vec4(0.0);
    vec2 r = vTexCoord - vec2(0.5);
    float d = length(r);
    if (d > size && d < 0.5) {
        float a = atan(r.y, r.x);
        if (a2 > a1) {
            if (a > a1 && a < a2) {
                col = color;
            }
        } else {
            if (a > a1 || a < a2) {
                col = color;
            }
        }
    }
    gl_FragColor = col;
}

]]
--# Timer
Timer = class()

function Timer:init(time,x,y,tbl)
    -- you can accept and set parameters here
    local tbl = tbl or {}
    self.paused = true
    self.startTime = 0
    self.tickTime = 0
    self.time = time or 60
    self.increment = math.rad(360/ (self.time *100))

    self.radius = tbl.radius or 200
    self.width = tbl.width or 25
    --Colors
    self.startColor = tbl.startColor or color(0, 255, 0, 255)
    self.midColor = tbl.midColor or color(255, 255, 0, 255)
    self.endColor = tbl.endColor or color(255, 0, 0, 255)
    self.x = x or WIDTH/2
    self.y = y or HEIGHT/2
    self.m = mesh()
    self.m:addRect(0,0, self.radius * 2, self.radius * 2)
    self.m.shader = shader(Shader.vertex,Shader.fragment)
    self.m.shader.size = (1 - self.width/self.radius) * 0.5
    self.m.shader.a1 = math.rad(180)
    self.m.shader.a2 = math.rad(180)
    
end

function Timer:draw()
    -- Codea does not automatically call this method
    if not self.paused then
        local t = tonumber(string.format("%.2f",ElapsedTime))

        
        if t > self.startTime +1 then
            self.startTime = t
            self.time = self.time - 1
        end
        if t > self.tickTime then
            self.tickTime = t
            self.m.shader.a2 = self.m.shader.a2 - self.increment
        end
    end
    pushMatrix()
    translate(self.x,self.y)
    
    local clr
    if math.deg(self.m.shader.a2) > 0 then
        clr = self.startColor
    elseif math.deg(self.m.shader.a2) <=0 and math.deg(self.m.shader.a2) > -90 then
        clr = self.midColor
    else
        clr = self.endColor
    end
    self.m.shader.color = clr
    self.m:draw()
    popMatrix()
end

function Timer:touched(touch)
    -- Codea does not automatically call this method
end

function Timer:start()
    self.startTime = ElapsedTime
    self.paused = false
    
end

Glad to see my ideas in action. I used the same method as @JakAttak and @Briarfox in StackIt.

@Chipis - it would be much easier to use a timer where the circle shrank from the outside toward the middle (by just drawing a smaller and smaller circle). I believe it’s usually best to work to the strengths of your program (ie stick to what it can do easily, where possible).

Otherwise, this is a huge amount of work to do, just for a tiny part of your game.

@zoyt I didn’t even notice that @jakattak used the shader :slight_smile: mine isn’t done but thats the best I could do before heading to bed :slight_smile:

Thank you guys for the posts, I now have a better idea where I’m going with this timer. Don’t go too far, I’m sure I’ll have more questions in the near future.