Another take on Linear Gradient fill

I’ve been thinking again lately about gradient filled boxes / rects in Codea especially with a view to rendering assets at runtime to cut down on space and I came up with the following class.

What it does is generates a single pixel wide (or high) image of a given size (use either width or height in params table) and fills it with a RGB[A] gradient.

As you can see from the demo below - the gradient is defined by a table of stops, where each entry is a table of values, the first is simply a float value from 0 to 1 and represents the position of the stop in the output and the remaining 3 / 4 entries correspond to the RGB / RGBA values at that point. The code interpolates between all the stops in the table to produce the image.

There’s no error checking on the stop position, but the first one should be at 0 and the last one should be at 1 or else it won’t match up to the output image.

The output image can be any size, but the bigger the image the higher the resolution of the gradient and the less banding you see when you scale the output. I’ve included a reasonable example in the class if you don’t specify any params, but ideally you’d pass in your own parameters.

You can then use the image as a sprite (as shown) or as texture for a mesh / shader, it might even be able to generate the lighting table for a toon shader (but I haven’t got that far yet).

-- Gradient
-- A utility class to take a table of color stops (in the range 0-1) and returns an image that can be used as a texture

Gradient = class()

function Gradient:init(params)
    params = params or { height=100, stops = {
        {0,   255,  0,  0,255},
        {0.2,   0,255,  0,128},
        {0.4,   0,  0,255,255},
        {0.6, 255,  0,255,255},
        {0.8, 255,255,  0,192},
        {1,   255,255,255,255}}
    }
    
    local stops = params.stops or {
        {0,  255,0,0},
        {1,  0,255,0}
    }
    local w = params.width or 1
    local h = params.height or 1
    local img = image(w,h)
    local x,xi,y,yi = 1,0,1,0
    local len

    if params.width then xi,len = 1,w else yi,len = 1,h end
     
    -- Render the texture
    local st,en,s1,s2,range
    local r,g,b,a,ri,gi,bi,ai
    
    for i=1,#stops-1 do
        -- work out the steps / range
        s1,s2 = stops[i], stops[i+1]
        st,en = s1[1] * len, s2[1] * len
        range = en-st
        
        r = s1[2]; ri = (s2[2]-r) / range
        g = s1[3]; gi = (s2[3]-g) / range
        b = s1[4]; bi = (s2[4]-b) / range
        
        if s1[5] then a = s1[5]; ai = (s2[5]-a) / range
        else a,ai = 255,0
        end
        
        -- then render it
        for j=st,en do
            img:set(x,y,r,g,b,a)
            x = x + xi; y = y + yi
            r = r + ri; g = g + gi; b = b + bi; a = a + ai 
        end
    end
    
    self.img = img
end

function Gradient:getImage() return self.img end

And here is a demo program to show it off

-- Use this function to perform your initial setup
function setup()
    g1 = Gradient()    
    g2 = Gradient({ width=200, 
        stops = {
        {0,   255,  0,  0,255},
        {0.5,   0,255,  0,255},
        {1,     0,  0,255,255}}})
end

-- This function gets called once every frame
function draw()
    background(40, 40, 50)    
    spriteMode(CORNER)
    sprite(g1:getImage(),50,50,50,HEIGHT-100)
    sprite(g2:getImage(),150,500,500,50)
end

Note : that if you don’t pass any params in, it’ll use the default one setup in the class, also use either width OR height.

This just gives a simple horizontal / vertical linear gradient, if you need to display it rotated, you could just draw a rect that was big enough and then use clipping to actually display the required area.

My original intention was to use this with a modified ellipse shader to render nice gradient filled rounded rectangles, but if anyone want’s to tackle this then be my guest :slight_smile:

Enjoy…

Hi @TechDojo,

Very neat and impressive. I have also been working on gradient fills and have a messy code version that works up to a point. I need to tidy and expand it so that I can change the gradient using parameters (Codea parameters that is).

I started with my own - drawing lines to fill in images, then I used Simeons routine for the fill which is great, compact and fast enough for my needs.

Having said that I think I’ll try out my system using parts of your code.

Great work, thanks for the ideas.

Bri_G

:smiley:

Hi @Bri_G,
Somewhere I must have missed @Simeon’s example and I couldn’t find any reference to the raw version of the image functions (guess it helps if you wrote the system and know all the little tricks :slight_smile: )

That said - unless I’m missing something - @Simeon’s gradient function actually returns a 64x64 block with a single gradient, where the colour is calculated for every pixel across the scanline - so that’s 64x the calculations that aren’t required and also 64x times the storage space for the image :slight_smile: - Still a very neat demo and a good way to demonstrate coding art rather than relying on bulky assets.

So I’m guessing the rawWidth / height and rawSet actually ignore the retina settings and work directly with the surface - that’s useful to know.

The real advantage with my code I think is the ability to have as many stop points as you want to create any kind of linear gradient and the ability to define the resolution by specifying the width / height.

Anyway - glad you liked it - I’m working on a spherical one to render shaded balls / marbles for another idea I have.

Here’s another little demo that utilises the Gradient library - either set a dependancy on it or replace the main tab above with this…

-- RasterBars - old skool demo

-- Use this function to perform your initial setup
function setup()
   displayMode(FULLSCREEN)

   centerX = WIDTH * 0.5
   centerY = HEIGHT * 0.5
    
    barImg = Gradient({ height=100, 
             stops = {{0, 0,0,0,0},{0.5,255,255,255,255},{1,0,0,0,0}}}):getImage()
    numBars = 20
    barOffsetScale = 3
    iI = 1 / numBars
end

-- This function gets called once every frame
function draw()

    local t = ElapsedTime * 2
    local x,y,ni,r,g,b

    background(0, 0, 0)
    
    -- Do the raster bar effect
    ni = 0
    for i=1,numBars do
        y = centerY + (centerY * math.cos(t + (ni * barOffsetScale)))

        -- Dynamically adjust the colour
    	r = 192 + (64 * math.sin(t*3.7+ni*1.3))
		g = 192 + (64 * math.sin(t*3.7+ni*4.2))
		b = 192 + (64 * math.cos(t*3.7+ni*3.7))
		
        tint(r,g,b)
        sprite(barImg,centerX,y,WIDTH,100)
        
        ni = ni + iI
    end

    -- 
    noTint()
    sprite("Cargo Bot:Codea Icon",centerX,centerY+100)
    sprite("Cargo Bot:Codea Logo",centerX,centerY-100)
end

Here’s my Gradient Fill shader, just to add another to the list. It’s only a simple fill in that it interpolates between colours at the corners, but it has the benefit of working for arbitrary convex quadrilaterals, not just rectangles.

Wow talk about bringing a gun (or a nuke) to a knife fight :slight_smile:
I started to read your post but then my brain melted :slight_smile: - I did however get the code to run and I’ll be pulling apart the shader (with assitance from your post) to try and understand what’s going on.

Thanks for the example