A more accurate slider, free to use!

I meant to do this for a while but work and procrastination slowed me down. I had the trouble of not being able to accurately set the value on small sliders, to combat this I have created a slider that expands to give a much better accuracy. You can change the expand/contract animations by editing the easing in the tween functions. The relative mode makes it so the slider grip stays in the same place and moves the slider to compensate. Anyway here it is:

Slider = class()

function Slider:init(pos,width,sval,eval,val,...)
    if ... then self.text = ... else self.text = "" end
    self.val = val
    self.eval = eval
    self.sval = sval
    self.pos = pos
    if type(width) == "number" then
        self.width = width 
        self.w = {w = width}
    else
        self.width = width.x
        self.w = {w = width.x}
    end
    self.hold = nil
    self.txtcol = color(0,255)
    self.visible = true
    self.tween = nil
    self.res = 0
    self.type = "slider"
    self.info = nil
    self.holding = nil
end

function Slider:setVisible(bool)
    self.visible = bool
end

function Slider:textCol(col)
    self.txtcol = col
end

function Slider:setRes(res)
    self.res = res
end

function Slider:addZero() end

function Slider:draw()
    if not self.visible then return end
    local p,z,s,sv,ev,v = self.pos,self.width,self.w.w,self.sval,self.eval,self.val
    local h = (s/z)
    local ns = s/((ev-sv))
    local hl = self.hold
    if hl then
        hl = hl
        p = p + vec2((p.x-hl)*(h-1),0)
    end
    local tp = p-vec2((ns*(sv+ev)*0.5),0)+vec2(((s/(ev-sv)))*v,0)
    pushStyle()
        fill(50,h*70-70)
        rect(-10,-10,WIDTH+20,HEIGHT+20)
        lineCapMode(PROJECT)
        strokeWidth(h*30)
        stroke(0,h*70-70)
        line(p.x-s/2,p.y,p.x+s/2,p.y)
        lineCapMode(ROUND)
        strokeWidth(8+h*3)
        stroke(0,255)
        line(p.x-s/2,p.y,p.x+s/2,p.y)
        strokeWidth(7+h*3)
        stroke(255,255)
        line(p.x-s/2,p.y,p.x+s/2,p.y)
        fill(255,255)
        stroke(0,255)
        strokeWidth(1)
        ellipse(tp.x,tp.y,19+h*3)
        fill(255*(h-1),255)
        font("Marion-Bold")
        fontSize(12)
        text(math.floor((v)*(10^self.res))/(10^self.res),p.x,p.y-18-h*2)
        textMode(CORNER)
        text(self.text,p.x-s/2,p.y+10+h*2)
    popStyle()
end

function Slider:toucht(t)
    if not self.visible then return end
    if self.holding then return true end
    local p,s,z,sv,ev,v = self.pos,self.w.w,self.width,self.sval,self.eval,self.val
    local ns = ((ev-sv)/v)/s
    local h = (s/z)-1
    local hl = self.hold
    if hl then
        hl = hl
        p = p + vec2((p.x-hl)*(h),0)
    end
    --local tp = p-vec2(s/2,0)+vec2(((s/(ev-sv)))*v,0)
    local tp = p-vec2((ns*(sv+ev)*0.5),0)+vec2(((s/(ev-sv)))*v,0)
    if (t.x > p.x-s/2-h*20-20 and t.x < p.x+s/2+h*20+20
        and t.y > p.y-10-h*7.5 and t.y < p.y+10+h*7.5) or vec2(t.x,t.y):dist(tp)<15+h*2 then
        return true
    end
    return false
end

function Slider:getValue()
    return self.val
end

function Slider:touched(t)
    if not self.visible then return end
    local p,z,s,sv,ev,v = self.pos,self.width,self.w.w,self.sval,self.eval,self.val
    local ns = s/((ev-sv))
    local h = (s/z)
    local hl = self.hold
    if hl then
        hl = hl
        p = p + vec2((p.x-hl)*(h-1),0)
    end
    local tp = p-vec2(s/2,0)+vec2(((s/(ev-sv)))*v,0)
    local cp = p-vec2((ns*(sv+ev)*0.5),0)+vec2(((s/(ev-sv)))*v,0)
    local np,val
    if t.state == BEGAN and self:toucht(t) then
        if not self.hold then
            if self.tween then
                tween.stop(self.tween)
            end
            self.hold = cp.x
            self.tween = tween(0.5,self.w,{w = self.width*2},tween.easing.backInOut,function() 
                self.tween = nil
            end)
        end
        self.holding = true
    end
    if t.state == MOVING and self.hold and not self.tween and self.holding then
        np = t.x
        np = np - (p.x-s/2)
        val = ((ev-sv)/s)*np+sv
        self.val = math.ceil(((clamp(val,sv,ev)-0.5)*(10^self.res)))/(10^self.res)
    end
    if t.state == ENDED then
        self.holding = nil
        if not self.tween then
            self.tween = tween(0.25,self.w,{w = self.width},tween.easing.backOut,function() 
                self.hold = nil 
                self.tween = nil
            end)
        end
    end
end

Edit: fixed a bug.
Edit2: fixed another bug. Should work properly now.

simple and useful!

Slider using a shader for fun:


--# Main
-- Slider

-- Use this function to perform your initial setup
function setup()
    sl = Slider(vec2(WIDTH/2,HEIGHT/2),200,0,400,100,"Test Slider:")
    sl:textCol(color(255))
end

function touched(t)
    sl:touched(t)
end

function clamp(val,min,max)
    val = math.min(math.max(val,min),max)
    return val
end

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

    -- This sets the line thickness

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

shadr = {}
shadr.fs = [[
varying highp vec2 vTexCoord;
uniform lowp vec4 color;
uniform highp float time;
uniform highp float len;
void main()
{
    mediump vec2 vTexCoordn = 1.0-2.0*vTexCoord;
    lowp float dist = sqrt((vTexCoord.x-time)*(vTexCoord.x-time)+vTexCoordn.y*vTexCoordn.y);
    highp float mp = cos(vTexCoord.x-time)*len*dist*50.0;
    vTexCoordn.x = 0.0;
    //Premult
    gl_FragColor = color*(1.9-length(vTexCoordn)*1.7*mp);
}
]]
shadr.vs = [[
uniform mat4 modelViewProjection;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
void main()
{
    vColor = color;
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}]]
--# Slider
Slider = class()

function Slider:init(pos,width,sval,eval,val,...)
    if ... then self.text = ... else self.text = "" end
    self.val = val
    self.eval = eval
    self.sval = sval
    self.pos = pos
    if type(width) == "number" then
        self.width = width 
        self.w = {w = width}
    else
        self.width = width.x
        self.w = {w = width.x}
    end
    self.hold = nil
    self.txtcol = color(0,255)
    self.visible = true
    self.tween = nil
    self.res = 0
    self.type = "slider"
    self.info = nil
    self.m = mesh()
    self.r = self.m:addRect(pos.x,pos.y,width+20,300)
    self.m.shader = shader(shadr.vs,shadr.fs)
    self.m.shader.len = 10
    self.m.shader.color = vec4(0.2,0.3,0.9,1.0)
    self.m.shader.time = 0.1
end

function Slider:setVisible(bool)
    self.visible = bool
end

function Slider:textCol(col)
    self.txtcol = col
end

function Slider:setRes(res)
    self.res = res
end

function Slider:addZero() end

function Slider:draw()
    if not self.visible then return end
    local p,z,s,sv,ev,v = self.pos,self.width,self.w.w,self.sval,self.eval,self.val
    local h = (s/z)
    local ns = s/((ev-sv))
    local hl = self.hold
    if hl then
        hl = hl
        p = p + vec2((p.x-hl)*(h-1),0)
        self.m:setRect(self.r,p.x,p.y,s+20,300)
    end
    local tp = p-vec2((ns*(sv+ev)*0.5),0)+vec2(((s/(ev-sv)))*v,0)
    pushStyle()
        fill(50,h*70-70)
        rect(-10,-10,WIDTH+20,HEIGHT+20)
        lineCapMode(PROJECT)
        strokeWidth(h*30)
        stroke(0,h*70-70)
        line(p.x-s/2,p.y,p.x+s/2,p.y)
        --[[lineCapMode(ROUND)
        strokeWidth(8+h*3)
        stroke(30, 255)
        line(p.x-s/2,p.y,p.x+s/2,p.y)
        strokeWidth(7+h*3)
        stroke(120,255)
        line(p.x-s/2,p.y,p.x+s/2,p.y)
        fill(255,255)
        stroke(0,255)
        strokeWidth(1)
        ellipse(tp.x,tp.y,19+h*3)]]
        self.m:draw()
        fill(255*(h-1),255)
        font("Marion-Bold")
        fontSize(12)
        text(math.ceil(v*(10^self.res))/(10^self.res),p.x,p.y-22-h*2)
        textMode(CORNER)
        text(self.text,p.x-s/2,p.y+10+h*2)
    popStyle()
end

function Slider:toucht(t)
    if not self.visible then return end
    local p,s,z,sv,ev,v = self.pos,self.w.w,self.width,self.sval,self.eval,self.val
    local ns = ((ev-sv)/v)/s
    local h = (s/z)-1
    local hl = self.hold
    if hl then
        hl = hl
        p = p + vec2((p.x-hl)*(h),0)
    end
    --local tp = p-vec2(s/2,0)+vec2(((s/(ev-sv)))*v,0)
    local tp = p-vec2((ns*(sv+ev)*0.5),0)+vec2(((s/(ev-sv)))*v,0)
    if (t.x > p.x-s/2-h*20-20 and t.x < p.x+s/2+h*20+20
        and t.y > p.y-10-h*7.5 and t.y < p.y+10+h*7.5) or vec2(t.x,t.y):dist(tp)<15+h*2 then
        return true
    end
    return false
end

function Slider:getValue()
    return self.val
end

function Slider:touched(t)
    if not self.visible then return end
    local p,z,s,sv,ev,v = self.pos,self.width,self.w.w,self.sval,self.eval,self.val
    local ns = s/((ev-sv))
    local h = (s/z)
    local hl = self.hold
    if hl then
        hl = hl
        p = p + vec2((p.x-hl)*(h-1),0)
    end
    local tp = p-vec2(s/2,0)+vec2(((s/(ev-sv)))*v,0)
    local cp = p-vec2((ns*(sv+ev)*0.5),0)+vec2(((s/(ev-sv)))*v,0)
    local np,val
    if t.state == BEGAN and self:toucht(t) and not self.hold then
        if self.tween then
            tween.stop(self.tween)
        end
        self.hold = cp.x
        self.tween = tween(0.5,self.w,{w = self.width*2},tween.easing.quintInOut,function() 
            self.tween = nil
        end)
    end
    if t.state == MOVING and self.hold and not self.tween then
        np = t.x
        np = np - (p.x-s/2)
        val = ((ev-sv)/s)*np+sv
        self.val = math.ceil((clamp(val,sv,ev)*(10^self.res)))/(10^self.res)
        self.m.shader.time = (self.val/(ev-sv))*h*0.4+0.1
    end
    if t.state == ENDED and not self.tween then
        self.tween = tween(0.25,self.w,{w = self.width},tween.easing.backOut,function() 
            self.hold = nil 
            self.tween = nil
        end)
    end
end

Could use it if I could make it look better…

@luatee maybe i’m doing something wrong, but still bugs for me on touched ended

@Luatee If you tap the slider, you get a touch BEGAN, but you don’t get a touch ENDED and the slider stays selected. Not sure if that’s what you’re after.

@piinthesky @dave1707 sorry more clear now, no this is the wanted behaviour. You can change this by making the tween shorter, the reason I did it like this was for a smoother animation. If you wait until it springs out then stop touching it should work properly.

You could disable this by taking out ‘and not self.tween’ in the ENDED part of the Slider touched function. You’ll see why it’s not the desired functionality but it could be worked around I guess.