Multiple step colour gradient shader

I’m working on a platform game with large, vertically scrolling levels and wanted a simple parallax sky effect in the background. Using a large texture would be impractical as the levels are going to be considerably taller than 2048 pixels. So I wrote a shader for shading between multiple colours. You set 5 colours, the percentage points (from scale 0.0 to 1.0) at which you’d like one to fade in the next, the centre point at which the gradient starts, and the shape of the gradient (circle, elliptical, or linear gradients are possible). I thought I’d share it here in case someone else has a use for it. This could be adapted for all sorts of things, eg to make colourful text etc.

The image below is my attempt at a sunset sky. It would look pretty good (in a cartoony way) with some clouds in front of it.

gradient


--# Main
-- MultiStop Colour Gradients

function setup()
    print ("Drag your finger to set the gradient centre")
    print ("Pull down this output window to access more settings")
    Gradient.setup()
    parameter.number("GradientAspect", 0, 2, 0.5)
    parameter.number("Step1", 0, 1, 0.9)
    parameter.number("Step2", 0, 1, 0.3)
    parameter.number("Step3", 0, 1, 0.2)
    parameter.number("Step4", 0, 1, 0.1)
    parameter.number("Step5", 0, 1, 0.05)
    
    parameter.color("Color1", 
    color(31, 27, 53, 255))
    parameter.color("Color2", 
    color(0, 198, 255, 255))
    parameter.color("Color3", 
    color(228, 207, 225, 255))
    parameter.color("Color4", 
    color(254, 0, 0, 255))
    parameter.color("Color5", 
    color(255, 255, 1, 255))

    Centre = vec2(0,-0.4)
end

function draw()
    background(40, 40, 50)
    Gradient.mesh.shader.centre = Centre
    Gradient.mesh.shader.aspect = vec2(GradientAspect, 1) 
    Gradient.mesh.shader.colors = {Color1, Color2, Color3, Color4, Color5}
    Gradient.mesh.shader.step = {Step1, Step2, Step3,  Step4, Step5}
    Gradient.mesh:draw()
end

function touched(t)
    Centre = vec2(-0.5 + t.x/ WIDTH , -0.5 + t.y/HEIGHT)
end


--# Gradient
Gradient = {}

function Gradient.setup()
    Gradient.mesh = mesh()
    Gradient.mesh:addRect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
    Gradient.mesh.shader = shader(
    [[
    uniform mat4 modelViewProjection;
    uniform lowp vec2 aspect; // set to (1,1) for circular gradient, (0,1) or (1,0) for linear, or fractional, eg (1, 0.5) for oval
        
    attribute vec4 position;
    attribute vec4 color;
    attribute vec2 texCoord;
    
   // varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    
    void main()
    {
       // vColor = color;
        vTexCoord = (texCoord - vec2(0.5,0.5)) * aspect;
        gl_Position = modelViewProjection * position;
    }
    ]],
    [[
    precision highp float;
    
    const int no = 5; //the number of gradation points you want to have
    
    // uniform lowp sampler2D texture;
    uniform lowp vec2 centre; // centre of gradient
    uniform float step[no];  // transition points between colours, 0.0 - 1.0
    uniform lowp vec4 colors[no]; // colours to grade between
    
    //varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    varying float vDistance;
    
    void main()
    {
        float dist = distance( vTexCoord, centre);
        lowp vec4 col = colors[0];
        for (int i=1; i<no; ++i) {
            col = mix(col, colors[i], smoothstep(step[i-1], step[i], dist));
        }
        gl_FragColor = col; //texture2D( texture, vTexCoord ) * col;
    }
    ]]
    )

end

Here’s a variant for shading text:

text

-- MultiStop Colour Gradients

displayMode(FULLSCREEN)
supportedOrientations(LANDSCAPE_ANY)

function setup()
    font("DINAlternate-Bold")
    fontSize(70)
    textAlign(CENTER)
    textMode(CORNER)
    textWrapWidth(WIDTH*0.8)
    fill(255)

    Gradient.setup("Does anyone remember the Commodore Amiga? It had a chip called the COPPER. This could change the colour value each line of the display. I still think of colour gradients like these as “COPPER bars” \\u{1f61d}")
end

function draw()
    background(0)
    Gradient.mesh:draw()
end

Gradient = {}

function Gradient.setup(txt)
   
    local w,h = textSize(txt)
    local img = image(w,h)
    setContext(img)
    text(txt)
    setContext()
    Gradient.mesh = mesh()
    Gradient.mesh.texture = img
    Gradient.mesh:addRect(WIDTH/2, HEIGHT/2, w,h)
    Gradient.mesh.shader = shader(
    [[
    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; //- vec2(0.5,0.5);
        gl_Position = modelViewProjection * position;
    }
    ]],
    [[
    precision highp float;
    
    const int no = 6; //the number of gradation points you want to have
    
    uniform lowp sampler2D texture;
    uniform float step[no];  // transition points between colours, 0.0 - 1.0
    uniform lowp vec4 colors[no]; // colours to grade between
    uniform float repeats;
        
    //varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    varying float vDistance;
    
    void main()
    {
        lowp vec4 col = colors[0];
        for (int i=1; i<no; ++i) {
            col = mix(col, colors[i], smoothstep(step[i-1], step[i], fract(vTexCoord.y*repeats)));
        }
        gl_FragColor = texture2D( texture, vTexCoord ) * col;
    }
    ]]
    )
    Gradient.mesh.shader.colors = {
    color(27, 29, 50, 255),
    color(41, 164, 218, 255),
    color(222, 231, 255, 255),
    color(79, 39, 31, 255),
    color(188, 53, 53, 255),
    color(255, 255, 100, 255)}
    Gradient.mesh.shader.step = {0.8, 0.5, 0.45, 0.43, 0.35, 0.09}
    Gradient.mesh.shader.repeats = 8 --how many times the texture repeats. set this to the number of lines
end

Ah, the wonderful days of the 80’s. Personally, I think that gradient text is hard to read.

Please just don’t write code for flashing text. [-O<

Yes, I wouldn’t typeset a novel like this

( instead I’d use this: http://codea.io/talk/discussion/6710/markdown-codea-rich-text-formatting#latest )

This is for attention-grabbing. Hi-score tables and the like.

beautiful Colour Gradients text

@yojimbo2000 been messing about with your first gradient shader. Looking to see if I can go for some varying backdrops for my ghost slicing game. Here’s a graveyard scene


--# Main
-- MultiStop Colour Gradients

function setup()
    print("Drag down for more options")
    Gradient.setup()
    parameter.number("GradientAspect", 0, 2, 1)
    parameter.number("Step1", 0, 1, 0.6)
    parameter.number("Step2", 0, 1, 0)
    parameter.integer("MoonMove",0,1,1)
    
    parameter.color("Color1",
    color(31, 27, 53, 255))
    parameter.color("Color2",
    color(0, 198, 255, 255))
    
    Centre = vec2(0,0)
    moon=vec2(WIDTH/2,HEIGHT/2)
    bgobj={}
    numobj=15
    for i=1,numobj do
        local lev=1
        if i>numobj*0.8 then
            lev=3
        elseif i>numobj*0.5 then
            lev=2
        end
        table.insert(bgobj,{x=math.random(WIDTH),y=75,level=lev,angle=-8+math.random(15),type=math.random(2)})
    end
    
    cross=image(100,200)
    setContext(cross)
    fill(255)
    rect(40,0,20,200)
    rect(0,140,100,20)
    setContext()
    
    gravestone=image(100,200)
    setContext(gravestone)
    fill(255)
    rect(10,0,80,120)
    ellipse(50,100,80)
    setContext()
    
    rail=image(60,200)
    setContext(rail)
    fill(255)
    rect(25,0,10,140)
    rect(-10,110,80,5)
    rect(-10,100,80,5)
    ellipse(30,145,20)
    setContext()
    
    img={}
    table.insert(img,{img=cross,w=100,h=200})
    table.insert(img,{img=gravestone,w=100,h=200})
end

function draw()
    if MoonMove==1 then
        Centre.y=math.sin(ElapsedTime*0.5)/2
        Centre.x=-math.cos(ElapsedTime*0.5)/3
    end
    moon.x=(Centre.x+0.5)*WIDTH
    moon.y=(Centre.y+0.5)*HEIGHT
    --shimmer on the moon
    --  Step1=0.3+math.random(100)/10000
    background(40, 40, 50)
    Gradient.mesh.shader.centre = Centre
    Gradient.mesh.shader.aspect = vec2(GradientAspect, 1)
    Gradient.mesh.shader.colors = {Color1, Color2}
    Gradient.mesh.shader.step = {Step1, Step2}
    Gradient.mesh:draw()
    fill(255)
    ellipse(moon.x,moon.y,80)
    
    for i,b in pairs(bgobj) do
        if b.level==3 then
            tint(0,255)
        elseif b.level==2 then
            local a=0.4
            tint(Color1.r*a,Color1.g*a,Color1.b*a,255)
        else
            local fade=math.max(255-(moon.y/HEIGHT)*255,100)
            tint(0,fade)
        end
        pushMatrix()
        translate(b.x,b.y*b.level/3)
        rotate(b.angle)
        sprite(img[b.type].img,0,0,img[b.type].w*b.level/3,img[b.type].h*b.level/3)
        popMatrix()
        noTint()
    end
    tint(0,255)
    for i=0,WIDTH,60 do
        sprite(rail,i,100)
    end
    noTint()
end

function touched(t)
    Centre = vec2(-0.5 + t.x/ WIDTH , -0.5 + t.y/HEIGHT)
end


--# Gradient
Gradient = {}

function Gradient.setup()
    Gradient.mesh = mesh()
    Gradient.mesh:addRect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
    Gradient.mesh.shader = shader(
    [[
    uniform mat4 modelViewProjection;
    uniform lowp vec2 aspect; // set to (1,1) for circular gradient, (0,1) or (1,0) for linear, or fractional, eg (1, 0.5) for oval
    
    attribute vec4 position;
    attribute vec4 color;
    attribute vec2 texCoord;
    
    // varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    
    void main()
    {
    // vColor = color;
    vTexCoord = (texCoord - vec2(0.5,0.5)) * aspect;
    gl_Position = modelViewProjection * position;
    }
    ]],
    [[
    precision highp float;
    
    const int no = 2; //the number of gradation points you want to have
    
    // uniform lowp sampler2D texture;
    uniform lowp vec2 centre; // centre of gradient
    uniform float step[no];  // transition points between colours, 0.0 - 1.0
    uniform lowp vec4 colors[no]; // colours to grade between
    
    //varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    varying float vDistance;
    
    void main()
    {
    float dist = distance( vTexCoord, centre);
    lowp vec4 col = colors[0];
    for (int i=1; i<no; ++i) {
    col = mix(col, colors[i], smoothstep(step[i-1], step[i], dist));
    }
    gl_FragColor = col; //texture2D( texture, vTexCoord ) * col;
    }
    ]]
    )
    
end

That’s awesome! Love how it’s all procedural. There’s some very atmospheric games around which are just silhouettes over a gradient. One on iOS called Volt that I like.

Although, there’s no need to draw the moon with an ellipse, do it with the shader. Try this:


--# Main
-- MultiStop Colour Gradients

function setup()
    print("Drag down for more options")
    Gradient.setup()
    parameter.number("GradientAspect", 0, 2, WIDTH/HEIGHT)
    parameter.number("Step1", 0, 1, 0.6)
    parameter.number("Step2", 0, 1, 0.06)
    parameter.number("Step3", 0, 1, 0.05)
    parameter.integer("MoonMove",0,1,1)

    parameter.color("Color1",
    color(31, 27, 53, 255))
    parameter.color("Color2",
    color(0, 198, 255, 255))
    parameter.color("Color3",
    color(255, 255, 255, 255))
    Centre = vec2(0,0)
    moon=vec2(WIDTH/2,HEIGHT/2)
    bgobj={}
    numobj=15
    for i=1,numobj do
        local lev=1
        if i>numobj*0.8 then
            lev=3
        elseif i>numobj*0.5 then
            lev=2
        end
        table.insert(bgobj,{x=math.random(WIDTH),y=75,level=lev,angle=-8+math.random(15),type=math.random(2)})
    end

    cross=image(100,200)
    setContext(cross)
    fill(255)
    rect(40,0,20,200)
    rect(0,140,100,20)
    setContext()

    gravestone=image(100,200)
    setContext(gravestone)
    fill(255)
    rect(10,0,80,120)
    ellipse(50,100,80)
    setContext()

    rail=image(60,200)
    setContext(rail)
    fill(255)
    rect(25,0,10,140)
    rect(-10,110,80,5)
    rect(-10,100,80,5)
    ellipse(30,145,20)
    setContext()

    img={}
    table.insert(img,{img=cross,w=100,h=200})
    table.insert(img,{img=gravestone,w=100,h=200})
end

function draw()
    if MoonMove==1 then
        Centre.y=math.sin(ElapsedTime*0.5)/2
        Centre.x=-math.cos(ElapsedTime*0.5)/3
    end
    moon.x=(Centre.x+0.5)*WIDTH
    moon.y=(Centre.y+0.5)*HEIGHT
    --shimmer on the moon
    --  Step1=0.3+math.random(100)/10000
    background(40, 40, 50)
    Gradient.mesh.shader.centre = Centre
    Gradient.mesh.shader.aspect = vec2(GradientAspect, 1)
    Gradient.mesh.shader.colors = {Color1, Color2, Color3}
    Gradient.mesh.shader.step = {Step1, Step2, Step3}
    Gradient.mesh:draw()
    fill(255)
 --   ellipse(moon.x,moon.y,80)

    for i,b in pairs(bgobj) do
        if b.level==3 then
            tint(0,255)
        elseif b.level==2 then
            local a=0.4
            tint(Color1.r*a,Color1.g*a,Color1.b*a,255)
        else
            local fade=math.max(255-(moon.y/HEIGHT)*255,100)
            tint(0,fade)
        end
        pushMatrix()
        translate(b.x,b.y*b.level/3)
        rotate(b.angle)
        sprite(img[b.type].img,0,0,img[b.type].w*b.level/3,img[b.type].h*b.level/3)
        popMatrix()
        noTint()
    end
    tint(0,255)
    for i=0,WIDTH,60 do
        sprite(rail,i,100)
    end
    noTint()
end

function touched(t)
    Centre = vec2(-0.5 + t.x/ WIDTH , -0.5 + t.y/HEIGHT)
end


--# Gradient
Gradient = {}

function Gradient.setup()
    Gradient.mesh = mesh()
    Gradient.mesh:addRect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
    Gradient.mesh.shader = shader(
    [[
    uniform mat4 modelViewProjection;
    uniform lowp vec2 aspect; // set to (1,1) for circular gradient, (0,1) or (1,0) for linear, or fractional, eg (1, 0.5) for oval

    attribute vec4 position;
    attribute vec4 color;
    attribute vec2 texCoord;

    // varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;

    void main()
    {
    // vColor = color;
    vTexCoord = (texCoord - vec2(0.5,0.5)) * aspect;
    gl_Position = modelViewProjection * position;
    }
    ]],
    [[
    precision highp float;

    const int no = 3; //the number of gradation points you want to have

    // uniform lowp sampler2D texture;
    uniform lowp vec2 centre; // centre of gradient
    uniform float step[no];  // transition points between colours, 0.0 - 1.0
    uniform lowp vec4 colors[no]; // colours to grade between

    //varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    varying float vDistance;

    void main()
    {
    float dist = distance( vTexCoord, centre);
    lowp vec4 col = colors[0];
    for (int i=1; i<no; ++i) {
    col = mix(col, colors[i], smoothstep(step[i-1], step[i], dist));
    }
    gl_FragColor = col; //texture2D( texture, vTexCoord ) * col;
    }
    ]]
    )

end

Of course - didn’t think of that!

Replace draw with the two functions below for a nice outline effect on the graveyard, which follows the direction of the moon

(This is a bit like Photoshop tennis, where you take a drawing and modify it, in turns).

function draw()
    if MoonMove==1 then
        Centre.y=math.sin(ElapsedTime*0.5)/2
        Centre.x=-math.cos(ElapsedTime*0.5)/3
    end
    moon.x=(Centre.x+0.5)*WIDTH
    moon.y=(Centre.y+0.5)*HEIGHT
    --shimmer on the moon
    --  Step1=0.3+math.random(100)/10000
    background(40, 40, 50)
    Gradient.mesh.shader.centre = Centre
    Gradient.mesh.shader.aspect = vec2(GradientAspect, 1)
    Gradient.mesh.shader.colors = {Color1, Color2, Color3}
    Gradient.mesh.shader.step = {Step1, Step2, Step3}
    Gradient.mesh:draw()
--get direction to moon, multiply by 3
    v=(moon-vec2(WIDTH/2,150)):normalize()*3
--draw white reflection offset by this vector
    DrawGraveyard(color(150),v.x,v.y)
--draw graveyard
    DrawGraveyard(color(0),0,0)
end

function DrawGraveyard(c,x,y)
    pushMatrix()
    translate(x,y)
    tint(c)
    for i,b in pairs(bgobj) do
        pushMatrix()
        translate(b.x,b.y*b.level/3)
        rotate(b.angle)
        sprite(img[b.type].img,0,0,img[b.type].w*b.level/3,img[b.type].h*b.level/3)
        popMatrix()
    end
    for i=0,WIDTH,60 do
        sprite(rail,i,100)
    end
    popMatrix()
    noTint()
end

Nice effect, though it cancels out the subtle fade out of the objects which are further in the distance.

In an action game, I’m not sure subtlety is a factor! :))

does it need the newest Codea version and ios or global replace?

it shows?shader compile error?attempt to redeclare ‘step’ as variable?attempt to use ‘step’ as variable… how to make it?

@firewolf - there are four versions of the code. Which one?

i tried again?the same errors in four versions

Which version of iOS and Codea are you using? Did you make any changes to the code before you saw that error?

I wonder whether older versions of open gl had a step function (in 2.0 it’s edge and smoothstep), which would make step not available as a variable name (similar to the issue where the reflect function was added).

thanks

Now with optional mist and rain


--# Main
-- MultiStop Colour Gradients

function setup()
    print("Drag down for more options")
    Gradient.setup()
    parameter.number("GradientAspect", 0, 2, WIDTH/HEIGHT)
    parameter.number("Step1", 0, 1, 0.6)
    parameter.number("Step2", 0, 1, 0.0501)
    parameter.number("Step3", 0, 1, 0.050)
    parameter.integer("MoonMove",0,1,1)
    parameter.number("rainangle",-30,30,-20)
    parameter.number("rainspeed",1.5,5,2)
    parameter.integer("MistOn",0,1,1)
    parameter.integer("RainOn",0,1,1)
    parameter.color("Color1",
    color(31, 27, 53, 255))
    parameter.color("Color2",
    color(0, 198, 255, 255))
    parameter.color("Color3",
    color(255, 255, 255, 255))
    Centre = vec2(0,0)
    moon=vec2(WIDTH/2,HEIGHT/2)
    bgobj={}
    numobj=15
    for i=1,numobj do
        local lev=1
        if i>numobj*0.8 then
            lev=3
        elseif i>numobj*0.5 then
            lev=2
        end
        table.insert(bgobj,{x=math.random(WIDTH),y=75,level=lev,angle=-8+math.random(15),type=math.random(2)})
    end
    
    cross=image(100,200)
    setContext(cross)
    fill(255)
    rect(40,0,20,200)
    rect(0,140,100,20)
    setContext()
    
    gravestone=image(100,200)
    setContext(gravestone)
    fill(255)
    rect(10,0,80,120)
    ellipse(50,100,80)
    setContext()
    
    rail=image(60,200)
    setContext(rail)
    fill(255)
    rect(25,0,10,140)
    rect(-10,110,80,5)
    rect(-10,100,80,5)
    ellipse(30,145,20)
    setContext()
    
    mist=image(200,30)
    setContext(mist)
    fill(255)
    polygon({vec2(10,5), vec2(100,25), vec2(190,20), vec2(110, 10)}) --must be in clockwise or anti-clockwise order
    setContext()
    
    rain=image(10,10)
    setContext(rain)
    fill(255)
    strokeWidth(4)
    stroke(255)
    line(5,1,5,9)
    setContext()
    
    img={}
    table.insert(img,{img=cross,w=100,h=200})
    table.insert(img,{img=gravestone,w=100,h=200})
    
    fog={}
    for i=1,30 do
        local fx=1
        if math.random(2)==1 then fx=-1 end
        local fy=1
        if math.random(2)==1 then fy=-1 end
        table.insert(fog,{x=math.random(WIDTH),y=math.random(HEIGHT/2),level=math.random(3),scalex=1+math.random(40)/10,fx=fx,fy=fy,spd=-3+math.random(60)/10})
    end
    
    raindrop={}
    for i=1,300 do
        table.insert(raindrop,{x=-300+math.random(WIDTH+600),y=math.random(HEIGHT),level=math.random(3)})
    end
    
end

function draw()
    if MoonMove==1 then
        Centre.y=math.sin(ElapsedTime*0.5)/2
        Centre.x=-math.cos(ElapsedTime*0.5)/3
    end
    moon.x=(Centre.x+0.5)*WIDTH
    moon.y=(Centre.y+0.5)*HEIGHT
    --shimmer on the moon
    --  Step1=0.3+math.random(100)/10000
    background(40, 40, 50)
    Gradient.mesh.shader.centre = Centre
    Gradient.mesh.shader.aspect = vec2(GradientAspect, 1)
    Gradient.mesh.shader.colors = {Color1, Color2, Color3}
    Gradient.mesh.shader.step = {Step1, Step2, Step3}
    Gradient.mesh:draw()
    fill(255)
    
    
    for i,b in pairs(bgobj) do
        if b.level==3 then
            tint(0,255)
        elseif b.level==2 then
            local a=0.4
            tint(Color1.r*a,Color1.g*a,Color1.b*a,255)
        else
            local fade=math.max(255-(moon.y/HEIGHT)*255,100)
            tint(0,fade)
        end
        pushMatrix()
        translate(b.x,b.y*b.level/3)
        rotate(b.angle)
        sprite(img[b.type].img,0,0,img[b.type].w*b.level/3,img[b.type].h*b.level/3)
        popMatrix()
        noTint()
    end
    
    
    --fog
    if MistOn==1 then
        for i,m in pairs(fog) do
            tint(255,(1-(m.y/(HEIGHT/2)))*40)
            sprite(mist,m.x,m.y,m.fx*200*m.scalex,m.fy*50)
            m.x = m.x + m.spd
            if m.x<-600 or m.x>WIDTH+600 then m.spd = m.spd * -1 end
            if m.spd==0 then m.spd=1 end
            noTint()
        end
    end
    
    if RainOn==1 then
        --lightning bolt
        if math.random(400)==1 then
            background(255)
        end
        
        for i,r in pairs(raindrop) do
            tint(0,70)
            pushMatrix()
            translate(r.x,r.y)
            rotate(rainangle)
            sprite(rain,0,0,10*r.level/2)
            popMatrix()
            r.x=r.x+rainspeed*r.level*math.sin(math.rad(rainangle))
            r.y=r.y-rainspeed*r.level*math.cos(math.rad(rainangle))
            if r.y<0 then
                r.y=HEIGHT+20
                r.x=-300+math.random(WIDTH+600)
            end
        end
    end
    tint(0,255)
    for i=0,WIDTH,60 do
        sprite(rail,i,100)
    end
    noTint()
end

function touched(t)
    Centre = vec2(-0.5 + t.x/ WIDTH , -0.5 + t.y/HEIGHT)
end


--# Gradient
Gradient = {}

function Gradient.setup()
    Gradient.mesh = mesh()
    Gradient.mesh:addRect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
    Gradient.mesh.shader = shader(
    [[
    uniform mat4 modelViewProjection;
    uniform lowp vec2 aspect; // set to (1,1) for circular gradient, (0,1) or (1,0) for linear, or fractional, eg (1, 0.5) for oval
    
    attribute vec4 position;
    attribute vec4 color;
    attribute vec2 texCoord;
    
    // varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    
    void main()
    {
    // vColor = color;
    vTexCoord = (texCoord - vec2(0.5,0.5)) * aspect;
    gl_Position = modelViewProjection * position;
    }
    ]],
    [[
    precision highp float;
    
    const int no = 3; //the number of gradation points you want to have
    
    // uniform lowp sampler2D texture;
    uniform lowp vec2 centre; // centre of gradient
    uniform float step[no];  // transition points between colours, 0.0 - 1.0
    uniform lowp vec4 colors[no]; // colours to grade between
    
    //varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    varying float vDistance;
    
    void main()
    {
    float dist = distance( vTexCoord, centre);
    lowp vec4 col = colors[0];
    for (int i=1; i<no; ++i) {
    col = mix(col, colors[i], smoothstep(step[i-1], step[i], dist));
    }
    gl_FragColor = col; //texture2D( texture, vTexCoord ) * col;
    }
    ]]
    )
    
end

--adapted from yojimbo2000
function polygon(points)
    if #points<3 then return end
    
    local verts=triangulate(points)
    local cols={}
    for a=1, #verts do
        cols[a]=color(fill())
    end
    local poly=mesh()
    poly.vertices=verts
    poly.colors=cols
    poly:draw()
    if strokeWidth()>0 then
        local a,b
        for a=1, #points do
            if a==#points then b=1 else b=a+1 end
            line(points[a].x,points[a].y,points[b].x,points[b].y)
        end
    end
end

Love it!