sparkles

I would like to speed up the following up, if possible. Basically it creates several fading “sparks” on the screen spawning them every xx seconds (based on “respawn” parameter). Each spark is following a cubic bezier curve with poles created in either random way or pre-defined.
If I put a respawn at 0.01 then my frame rate will drop to 30FPS. I have tried to use mesh and use local functions and parameters but the barrier of 30 FPS is still there.

spark = class()

function spark:init(length,dA,sA,mode,p0,p1,p2,p3)

        -- Cache functions
    local random = math.random
    local cos = math.cos
    local sin = math.sin
    local pi = math.pi
    
    -- you can accept and set parameters here
    self.m = mesh()
    local angle = sA or random(0,360) -- starting angle
    self.n = random(1,20)/100  -- tail length

    --define bezier points
    if mode == "random" then
        local margin = 100
        self.p0 = {x = random(0-margin,WIDTH+margin),y = random(0-margin,HEIGHT+margin)}
        angle = angle + random(-dA,dA)
        self.p1 = {x = self.p0.x + cos(angle*2*pi/360)*length, y = self.p0.y + sin(angle*2*pi/360)*length}
        angle = angle + random(-dA,dA)
        self.p2 = {x = self.p1.x + cos(angle*2*pi/360)*length, y = self.p1.y + sin(angle*2*pi/360)*length}
        angle = angle + random(-dA,dA)
        self.p3 = {x = self.p2.x + cos(angle*2*pi/360)*length, y = self.p2.y + sin(angle*2*pi/360)*length}
    else
        self.p0 = p0
        self.p1 = p1
        self.p2 = p2
        self.p3 = p3
    end

    self.time = ElapsedTime
    self.active = true 
end

function spark:draw()
    -- Codea does not automatically call this method

    -- Cache functions
    local atan2 = math.atan2
    local sqrt = math.sqrt
    local abs = math.abs
    local bez = bezier
    self.m:clear()
    local p0 = self.p0
    local p1 = self.p1
    local p2 = self.p2
    local p3 = self.p3
    local n = self.n
    
    if show_poles then
        pushStyle()
        fill(255, 0, 0, 100)
        ellipse(p0.x,p0.y,8)
        ellipse(p1.x,p1.y,8)
        ellipse(p2.x,p2.y,8)
        ellipse(p3.x,p3.y,8)
        strokeWidth(1)
        stroke(255,0,0,100)
        line(p0.x,p0.y,p1.x,p1.y)
        line(p1.x,p1.y,p2.x,p2.y)
        line(p2.x,p2.y,p3.x,p3.y)            
        popStyle()
    end
    
    local i = (ElapsedTime - self.time) / 4 -- sparkle life ... 4 seconds
    if i < 1 then
            for k = 0,n, 0.01 do
                if i+k < 1 then
                    local alpha = k/n*90*(-abs(0.5-i)*2+1) -- max alpha at i = 0.5, max alpha value = 90
                    local s = bez(p0,p1,p2,p3,i+k)
                    local f = bez(p0,p1,p2,p3,i+k+0.01)
                    local dx = f.x - s.x
                    local dy = f.y - s.y
                    local d = sqrt(dx * dx + dy * dy)
                    local a = atan2(dy, dx)
                    local idx = self.m:addRect(s.x + dx / 2, s.y + dy / 2, d, 2, a)    
                    self.m:setRectColor(idx,22,141,175,alpha)
                end
            end
            self.m:draw()
    else
        self.active = nil
    end
end

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

-- cubic bezier 
function bezier(p0,p1,p2,p3,mu)
    local c = {}
    c.x = 3 * (p1.x - p0.x)
    c.y = 3 * (p1.y - p0.y)
    local b = {}
    b.x = 3 * (p2.x - p1.x) - c.x
    b.y = 3 * (p2.y - p1.y) - c.y
    local a = {}
    
    a.x = p3.x - p0.x - c.x - b.x
    a.y = p3.y - p0.y - c.y - b.y
    
    local p={}
    p.x = a.x * mu * mu * mu + b.x * mu * mu + c.x * mu + p0.x
    p.y = a.y * mu * mu * mu + b.y * mu * mu + c.y * mu + p0.y

   return(p)
end

here is instead the call in main tab.
In setup():

    -- setup sparkles for background
    show_poles = nil
    length = 200    -- tail length
    dA = 10         -- angle variation
    sA = 45         -- starting angle
    respawn = 0.01   -- respawn frequence in seconds
    sparkles = {}
    spawncounter=0

and in draw():

    spawncounter = spawncounter + DeltaTime
    if spawncounter>respawn then
        table.insert(sparkles,spark(length,dA,sA,"random"))
        spawncounter=0
    end
    for k,s in pairs(sparkles) do
        if s.active then
            s:draw()
        else
            table.remove(sparkles,k)
        end
    end

Could you post a single copy-paste-run code?

just a little thing

you are repeating most of the work in bez when you don’t need to, and if you put s and f into a vec2, you could use the dist function to calculate the distance between them

Why not calc both s and f in bez, and use vec2 throughout, eg

--add delta parameter (0.01) *****
function bezier(p0,p1,p2,p3,mu,delta)
    --assuming p0, p1, p2, p3 are vec2 *****
    local c = 3*p1 - p0
    local b = 3*p2 - c
    local a = p3-p0-c-b

    p1=vec2(
        ((a.x * mu + b.x) * mu+ c.x) * mu + p0.x,
        ((a.y * mu + b.y )* mu + c.y )* mu + p0.y
        )
    --calculate again after adding delta *****
    mu=mu+delta
    p2=vec2(
        ((a.x * mu + b.x) * mu+ c.x) * mu + p0.x,
        ((a.y * mu + b.y )* mu + c.y )* mu + p0.y
        )
    --return p1 and distance from p2 ****
   return p1, p1:dist(p2)  
end

then back in draw, this

local s = bez(p0,p1,p2,p3,i+k)
local f = bez(p0,p1,p2,p3,i+k+0.01)
local dx = f.x - s.x
local dy = f.y - s.y
local d = sqrt(dx * dx + dy * dy)

becomes this instead

local s,d = bez(p0,p1,p2,p3,i+k,0.01)

I have put everything in a single copy-paste-run file and I have done the modifications suggested by Ignatz.
Only thing different is that I return p1 and p2 instead of the distance because i need the angle as well to align the mesh. It seems anyway I made a small mistake because the result is not exactly the same as before but I can correct it later. Anyway also with these modifications it’s still not improving that much (2-3 FPS).

function setup()
    show_poles = nil
    length = 200    -- tail length
    dA = 10         -- angle variation
    sA = 45         -- starting angle
    respawn = 0.01   -- respawn frequence in seconds
    sparkles = {}
    spawncounter=0

    -- setup FPS for debugging
    local x,y = WIDTH-30,HEIGHT-20
    local col = color(255)
    FPS(nil,col,x,y)
    
end


function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    -- This sets the line thickness
    strokeWidth(5)

    -- Do your drawing here
    spawncounter = spawncounter + DeltaTime
    if spawncounter>respawn then
        table.insert(sparkles,spark(length,dA,sA,"random"))
        spawncounter=0
    end
    for k,s in pairs(sparkles) do
        if s.active then
            s:draw()
        else
            table.remove(sparkles,k)
        end
    end    
end

spark = class()

function spark:init(length,dA,sA,mode,p0,p1,p2,p3)

        -- Cache functions
    local random = math.random
    local cos = math.cos
    local sin = math.sin
    local pi = math.pi
    
    -- you can accept and set parameters here
    self.m = mesh()
    local angle = sA or random(0,360) -- starting angle
    self.n = random(1,20)/100  -- tail length

    --define bezier points
    if mode == "random" then
        local margin = 100
        self.p0 = vec2 (
            random(0-margin,WIDTH+margin),
            random(0-margin,HEIGHT+margin)
            )
        angle = angle + random(-dA,dA)
        self.p1 = vec2 (
            self.p0.x + cos(angle*2*pi/360)*length,
            self.p0.y + sin(angle*2*pi/360)*length
            )
        angle = angle + random(-dA,dA)
        self.p2 = vec2 (
            self.p1.x + cos(angle*2*pi/360)*length,
            self.p1.y + sin(angle*2*pi/360)*length
            )
        angle = angle + random(-dA,dA)
        self.p3 = vec2 (
            self.p2.x + cos(angle*2*pi/360)*length,
            self.p2.y + sin(angle*2*pi/360)*length
            )
    else
        self.p0 = p0
        self.p1 = p1
        self.p2 = p2
        self.p3 = p3
    end

    self.time = ElapsedTime
    self.active = true 
end

function spark:draw()
    -- Codea does not automatically call this method

    -- Cache functions
    local atan2 = math.atan2
    local sqrt = math.sqrt
    local abs = math.abs
    local bez = bezier
    self.m:clear()
    local p0 = self.p0
    local p1 = self.p1
    local p2 = self.p2
    local p3 = self.p3
    local n = self.n
    
    if show_poles then
        pushStyle()
        fill(255, 0, 0, 100)
        ellipse(p0.x,p0.y,8)
        ellipse(p1.x,p1.y,8)
        ellipse(p2.x,p2.y,8)
        ellipse(p3.x,p3.y,8)
        strokeWidth(1)
        stroke(255,0,0,100)
        line(p0.x,p0.y,p1.x,p1.y)
        line(p1.x,p1.y,p2.x,p2.y)
        line(p2.x,p2.y,p3.x,p3.y)            
        popStyle()
    end
    
    local maxAlpha = 200    -- max alpha at i = 0.5, max alpha value = 90
    local i = (ElapsedTime - self.time) / 4 -- sparkle life ... 4 seconds
    if i < 1 then
            for k = 0,n, 0.01 do
                if i+k < 1 then
                    local alpha = k/n*maxAlpha*(-abs(0.5-i)*2+1)
                    local s,f = bez(p0,p1,p2,p3,i+k,0.01)
                    local d = s:dist(f)
                    local a = s:angleBetween(f)
                    local idx = self.m:addRect(s.x + d / 2, s.y + d / 2, d, 2, a)    
                    self.m:setRectColor(idx,22,141,175,alpha)
                end
            end
            self.m:draw()
    else
        self.active = nil
    end
end

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

-- cubic bezier 
function bezier(p0,p1,p2,p3,mu,delta)
    local c = 3*p1 - p0
    local b = 3*(p2-p1) - c
    local a = p3-p0-c-b
    
    local p1=vec2(
    ((a.x * mu + b.x) * mu + c.x) * mu + p0.x,
    ((a.y * mu + b.y) * mu + c.y) * mu + p0.y
    )
    mu = mu + delta
    local p2=vec2(
    ((a.x * mu + b.x) * mu + c.x) * mu + p0.x,
    ((a.y * mu + b.y) * mu + c.y) * mu + p0.y
    )    
   return p1, p2
end

You are drawing 4 ellipses and three lines for each spark, in each frame

It should be faster if you make a sprite (using setContext) for each spark when you create it, requiring only one drawing call per spark. This will require more memory, but I don’t think that should be a problem. Worth a try, anyway.

In general, I suggest what you should do is look for performance bottlenecks. For example, before trying my suggestion above, comment out all except one of the ellipses and lines, so you are only drawing one item per frame, run it, and if it makes a big difference, you know it is worth cutting down the drawing calls.

Similarly, to check if the bez function is the problem, replace it temporarily with some constant values to see the difference.

If you go through your code like this, taking things out and testing, you should identify the speed problem.

Thanks for your help! I’ll try step by step and see what can make a difference.

@Kotas btw you can automatically combine all the tabs into a single item on the clipboard by long-pressing the project on the project screen and selecting “copy”

I made a few small improvements. The biggest one, I think, is that instead of effectively remaking the mesh every frame, then it sets up the right number of rectangles and adjusts them. It’s not a huge gain, but it gains a few fps.

Other changes: shifted the function caching outside the draw function since doing it inside the draw function doesn’t save anything (you’re still doing the lookup when making the cache so if you only use it once, you’ve not saved any time). I’ve also cleaned up your use of vec2s: the point of using vec2s is to use their native methods. The last main change is to your for loop: you should loop over integers and adjust afterwards.

(Oh, and your FPS code was missing so I just put in a simple watch.)

That done, it’s still not much of an improvement. Much more is possible if you shift your code to a shader. Your code settles at about 90 sparkles, and each will on average have 10 trails. So there will be about 900 rectangles in your mesh. My explosion shader does something quite similar to what you’re doing, but with 9000 rectangles then the frame rate doesn’t drop at all. You can find an explanation of my explosion shader here.

function setup()
    show_poles = nil
    length = 200    -- tail length
    dA = math.pi/18         -- angle variation
    sA = math.pi/4         -- starting angle
    respawn = 0.01   -- respawn frequence in seconds
    sparkles = {}
    spawncounter=0

    -- setup FPS for debugging
    local x,y = WIDTH-30,HEIGHT-20
    local col = color(255)
    -- FPS(nil,col,x,y)
    parameter.watch("math.floor(1/DeltaTime)")
    parameter.watch("#sparkles")
end


function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    -- This sets the line thickness
    strokeWidth(5)

    -- Do your drawing here
    spawncounter = spawncounter + DeltaTime
    if spawncounter>respawn then
        table.insert(sparkles,spark(length,dA,sA,"random"))
        spawncounter=0
    end
    local rem,n = {},0
    for k,s in ipairs(sparkles) do
        if s.active then
            s:draw()
        else
            table.insert(rem,k)
            n = n + 1
        end
    end    
    for k=n,1,-1 do
        table.remove(sparkles,rem[k])
    end
end

-- cubic bezier 
function bezier(p0,p1,p2,p3,t)
    --[[
    local c = 3*p1 - p0
    local b = 3*(p2-p1) - c
    local a = p3-p0-c-b

    local p1=((a*mu +b)*mu +c)*mu +p0
    mu = mu + delta
    local p2=((a*mu +b)*mu +c)*mu +p0
    --]]
    local s = 1-t
    
   return t*t*t*p3 +3*s*t*t*p2 + 3*s*s*t*p1 + s*s*s*p0
end

spark = class()

    local random = math.random
    local cos = math.cos
    local sin = math.sin
    local pi = math.pi
    local atan2 = math.atan2
    local sqrt = math.sqrt
    local abs = math.abs
    local bez = bezier
    local maxAlpha = 200    -- max alpha at i = 0.5, max alpha value = 90

function spark:init(length,dA,sA,mode,p0,p1,p2,p3)
    self.m = mesh()
    local angle = sA or random()*2*pi -- starting angle
    self.n = random(1,20)  -- tail length
    for k=1,self.n do
        self.m:addRect(0,0,0,0)
    end

    --define bezier points
    if mode == "random" then
        local margin = 100
        self.p0 = vec2 (
            random(0-margin,WIDTH+margin),
            random(0-margin,HEIGHT+margin)
            )
        angle = angle + 2*random()*dA-dA
        self.p1 = self.p0 + vec2(length,0):rotate(angle)
        angle = angle + 2*random()*dA-dA
        self.p2 = self.p1 + vec2(length,0):rotate(angle)
        angle = angle + 2*random()*dA-dA
        self.p3 = self.p2 + vec2(length,0):rotate(angle)
    else
        self.p0 = p0
        self.p1 = p1
        self.p2 = p2
        self.p3 = p3
    end

    self.time = ElapsedTime
    self.active = true 
end

function spark:draw()
    local p0 = self.p0
    local p1 = self.p1
    local p2 = self.p2
    local p3 = self.p3
    local n = self.n

    if show_poles then
        pushStyle()
        fill(255, 0, 0, 100)
        ellipse(p0.x,p0.y,8)
        ellipse(p1.x,p1.y,8)
        ellipse(p2.x,p2.y,8)
        ellipse(p3.x,p3.y,8)
        strokeWidth(1)
        stroke(255,0,0,100)
        line(p0.x,p0.y,p1.x,p1.y)
        line(p1.x,p1.y,p2.x,p2.y)
        line(p2.x,p2.y,p3.x,p3.y)            
        popStyle()
    end


    local i = 100*(ElapsedTime - self.time) / 4 -- sparkle life ... 4 seconds
    local alpha,s,f,d,a
    if i < 100 then
            for k = 1,n do
                if i+k < 100 then
                    alpha = k/n*maxAlpha*(-abs(0.5-i/100)*2+1)
                    s,f = bez(p0,p1,p2,p3,(i+k)/100),bez(p0,p1,p2,p3,(i+k+1)/100)
                    d = s:dist(f)
                    a = s:angleBetween(f)
                    self.m:setRect(k,s.x + d / 2, s.y + d / 2, d, 2, a)    
                    self.m:setRectColor(k,22,141,175,alpha)
                else
                    self.m:setRect(k,0,0,0,0)
                end
            end
            self.m:draw()
    else
        self.active = nil
    end
end

nice?like meteor firework

@loopspace … thanks. I used your version and I see actually improvements even though as you said it’s not terrific.
I guess my next step is to look at shaders. I’m a beginner in programming so understanding shaders my take a while but your example seems pretty well explained.
I’ll give it a try!

The shader ebook here may help

https://www.dropbox.com/sh/mr2yzp07vffskxt/AACqVnmzpAKOkNDWENPmN4psa

I have tried some additional adjustment but at the end the main driver for speed is the for loop (with k parameter). If the step is changed from 1 to anything above the fps will increase substantially but I guess there is no easy way to optmize this.
So I’ll definitely need some study on shaders.
@Ignatz thanks for your ebook. I’ll do some training/learning on shaders and then I’ll try to solve this one.

Just a little thing: I changed the

a = s:angleBetween(f)

to

a = math.atan2(f.y - s.y, f.x - sx)

so that visually is better. Don’t know if there is a smarter way.

Ah yes, I should have put:

                    a = vec2(1,0):angleBetween(f-s)