Liquid line flow

Hello all, I just wanted to share the particle experiment I wrote last evening. The code is really short and simple (~50 lines) but you get some pretty interesting liquid like results with very little effort. Just touch the screen and watch the colors flow. If anybody is interested in what’s going on, i’d be happy to explain.

Would love to hear your thoughts and suggestions!

-- flowfield
function setup()
    backingMode(RETAINED)        -- retain screen pixels
    background(0, 0, 0, 255)     -- draw black background
    col = color(math.random(255), math.random(255), math.random(255))  -- set random line color
    parameter("swirls", .002, .02, .01)   -- declare interactive swirlyness parameter
    parameter("lineStrength", 1, 10, 2)   -- declare interactive line alpha parameter
    parameter("fadeScreen", 0, .2, 0)     -- declare interactive screen fade parameter
    watch("particles")           -- watch particle count
    touches = {}                 -- make container to track screen touches
    parts = {}                   -- make container for particles
    seed = math.random(1000)     -- randomise noise
end

function touched(touch)          -- track all screen touches
   if touch.state == ENDED  then
        touches[touch.id] = nil
        col = color(math.random(255), math.random(255), math.random(255), lineStrength*12.5)
    else touches[touch.id] = touch
    end
end

function draw()
    fill(0, 0, 0, fadeScreen*100)           -- set fill alpha acording to fade parameter
    rect(0,0, WIDTH, HEIGHT)          -- draw transparent rectangle to slowly fade screen
    particles = #parts                -- track nr of particles
    col.alpha = lineStrength*12.5     -- set line alpha to transparancy parameter
    fill(col)

    for i, p in pairs(parts) do                    -- loop through particles
        pixel = noise((seed+p.x)*swirls, (seed+p.y)*swirls)  -- get location of noisemap pixel
        brightness = (pixel+1) / 2 * p.mult        -- define brightness between 0-1
        speed = brightness * p.mult                -- get particle speed value
        size = 2 + speed/2 * p.mult                -- get particle size value
        angle = brightness * 360 * math.pi / 180   -- get angle based on pixel brightness
        p.x = p.x + math.cos(angle) * speed        -- move particle over X
        p.y = p.y + math.sin(angle) * speed        -- move particle over Y
        ellipse(p.x, p.y, size)                    -- draw particle
        
        if p.x < 0 or p.x > WIDTH or p.y < 0 or p.y > HEIGHT then
            table.remove(parts, i)                 -- remove particle if outside of screen
        end
    end
    
    for i, t in pairs(touches) do        -- add particles at touch position
        if t.state == MOVING or t.state == BEGAN then
            table.insert( parts, {x=t.x, y=t.y, mult=(1.5 + math.random() * 1.5)} )
        end
    end
end

I’m working on that :slight_smile: you just have to save your current move vector in the particle and then apply it as direction instead of the noise.

wow, really cool stuf…

Really Nice… It looks a lot like some of the Meritum Paint effects. I wonder if you can reproduce the neon glow from the Meritum app. Thanks for sharing the code.

Thanks for the commments, I just looked at the meritum app on youtube but couldn’t find the neon glow effect. Is it something like additive blending? I tried to find blending options in the codea drawing refference and found an old thread suggesting this option but I’m not sure if this was implemented.

Anybody know the status on that?

Hello @Kirl. That is a pretty combination of Perlin noise and particles.

I had these thoughts:

The speed of a particle can range from 0 to 3. Perhaps all particles should have a positive speed:


speed = brightness * p.mult + 1

The fading can suffer from the effects of rounding when alpha blending at very small alpha, discussed here. How about keeping the fade alpha constant at ~10 but applying it a different rates? For example:


local fadeTime = 0
function fader(fadeRate)
    if fadeRate < 10 then return end -- Turn off fading at low rates
    fadeTime = fadeTime + DeltaTime
    if fadeTime > 1/fadeRate then
        pushStyle()
        fill(0, 10)
        noStroke()
        rectMode(CORNER)
        rect(0, 0, WIDTH, HEIGHT)
        popStyle()
        fadeTime = fadeTime % (1/fadeRate)
    end
end

I am seeking functions that return a random colour from a spectrum of attractive colours. Recently, I have used random hues (from HSV):


function rH2RGB(alpha)
    local h1 = math.random() * 6
    local r, g, b
    local x = (1 - math.abs(h1 % 2 - 1))
    if h1 < 1 then r, g, b = 1, x, 0
    elseif h1 < 2 then r, g, b = x, 1, 0
    elseif h1 < 3 then r, g, b = 0, 1, x
    elseif h1 < 4 then r, g, b = 0, x, 1
    elseif h1 < 5 then r, g, b = x, 0, 1
    elseif h1 < 6 then r, g, b = 1, 0, x end
    return color(255 * r, 255 * g, 255 * b, alpha)
end

Thanks for your thoughts mpilgrem!

Why do you feel all particles should have a positive speed?

I’m aware of the rounding errors with low alpha values, but I thought it gave a pretty interesting effect, a bit like paint sinking into the screen. I was also kind of pleased with the short code, so I liked the single line solution.

I’ve been trying to find a way to select atractive colors too, so that will surely come in handy, thanks for sharing your aproach! :slight_smile:

@Kirk, that looks really good. I’m currently working on a fun painter with animated brushes. I will add this approach too, maybe experimenting with other noise functions. i have also tried higher values for the swirl, for 0.18 it looks like vasculature.

Sounds cool, would love to see your results KilamMalik!

.@Kirl, to select the neon glow effect (or sci-fi, or fur) in Meritum app, you have to select either wave or splash mode. These options don’t show up in the normal paint mode.

My comment on speed may be more theoretical than practical. It seemed to me, in principle, that particles could get ‘stuck’ in locations where noise() was -1 and speed was 0.

Having done some Monte Carlo analysis, it appears to me that the output of noise(x, y) may actually be -0.9 < noise < 0.999.

@Ric_Esrey
I have only watched meritum on youtube, I do not have the app.

@mpilgrem
I had not considered the speed problem before though, so thanks for mentioning (I wasn’t really thinking earlier)… Also huge thanks for the random hsv code, I’ve been trying hsv/rgb conversions before but never really got something working. Yours works great, can you explain the code a bit?

rH2RGB() is based on this Wikipedia article.

The three ‘co-ordinates’ r, g and b describe a cube of colours, with black at the origin and white at the opposite corner. Black and white each sit on 3 of the 12 edges of the cube. This function picks a random point on one of 6 remaining edges that circle the cube between those poles. Those points correspond to hue (H) in the HSV scheme, when S (saturation) and V (value) are both 1.

As h1 ranges from 0 up to 6, x ranges (linearly) from 0 to 1 and back to 0, three times. The final line scales up from a point on the unit cube to the range 0 to 255 that applies to each of the colour channels in practice.

@Kirl, your idea really inspired me :slight_smile: I’ve tried to paint with this function and changed the code a bit for that. Source below. I have added a frames-to-live for the particles. So they will disappear after a given number of frames. So it can be used to draw a line even with higher swirl values and it doesnt flow over the whole screen.

parameter seems not to allow values smaller than 0.01. So I had to add a swirlsFactor which is multiplied with swirls. So it is possible to set values between 0.001 and 100.

My biggest problem in creating a paint app out of this and my other brushes I already made is the retained mode → When I show a color select dialog, the picture is gone. I think I have to add a small button bar on top of the screen thats always visible. I’ve tried to paint in a texture, but thats too slow.

Picture with some samples:

Source:

-- flowfield
function setup()
    backingMode(RETAINED)        -- retain screen pixels
    background(0, 0, 0, 255)     -- draw black background
    col = color(math.random(255), math.random(255), math.random(255))  -- set random line color
    parameter("swirls", 0.01, 1, 0.1)   -- declare interactive swirlyness parameter
    parameter("swirlsFactor", 0.1, 100, 0.1)
    parameter("alpha", 1, 255, 128)   -- declare interactive line alpha parameter
    parameter("fadeScreen", 0, .2, 0)     -- declare interactive screen fade parameter
    parameter("framesToLive", 1, 600, 60) -- how many frames should the particles live.
    parameter("curlyness", 4, 16, 4) -- Multiplier for the 
    parameter("gspeed", 0.1, 5, 0.45) -- Global speed
    parameter("gsize", 0.1, 50, 1.5) -- Global size
    parameter("varyBrightness", 0.0, 1.0, 0.45) -- Amount of variation in brightness. 0 = no variation, 1 = variation down to black.
    watch("particles")           -- watch particle count
    touches = {}                 -- make container to track screen touches
    parts = {}                   -- make container for particles
end

function rH2RGB(alpha)
    local h1 = math.random() * 6
    local r, g, b
    local x = (1 - math.abs(h1 % 2 - 1))
    if h1 < 1 then r, g, b = 1, x, 0
    elseif h1 < 2 then r, g, b = x, 1, 0
    elseif h1 < 3 then r, g, b = 0, 1, x
    elseif h1 < 4 then r, g, b = 0, x, 1
    elseif h1 < 5 then r, g, b = x, 0, 1
    elseif h1 < 6 then r, g, b = 1, 0, x end
    return color(255 * r, 255 * g, 255 * b, alpha)
end

function touched(touch)          -- track all screen touches
   if touch.state == ENDED  then
        touches[touch.id] = nil
    elseif touch.state == BEGAN then
        col = rH2RGB(alpha)
        seed = math.random(1000)     -- randomise noise
    else touches[touch.id] = touch
    end
end

function draw()
    fill(0, 0, 0, fadeScreen*100)           -- set fill alpha acording to fade parameter
    rect(0,0, WIDTH, HEIGHT)          -- draw transparent rectangle to slowly fade screen
    particles = #parts                -- track nr of particles

    sw = swirls*swirlsFactor
    for i, p in pairs(parts) do                    -- loop through particles
        pixel = noise((p.s+p.x)*sw, (p.s+p.y)*sw)  -- get location of noisemap pixel
        pixelOne = (pixel + 1) / 2
        brightness = pixelOne        -- define brightness between 0-1
        speed = brightness * p.speedRnd             -- get particle speed value
        size = 2 + p.sizeRnd                -- get particle size value
        angle = math.rad(pixelOne * 360 * curlyness)   -- get angle based on pixel
        p.x = p.x + math.sin(angle) * speed        -- move particle over X
        p.y = p.y + math.cos(angle) * speed        -- move particle over Y
        c = color(p.c.r, p.c.g, p.c.b, p.c.a)
        c.a = (p.ftlCounter / p.ftl) * p.c.a
        fill(c)
        ellipse(p.x, p.y, size)                    -- draw particle
        p.ftlCounter = p.ftlCounter - 1
        
        if p.ftlCounter <= 0 or p.x < 0 or p.x > WIDTH or p.y < 0 or p.y > HEIGHT then
            table.remove(parts, i)                 -- remove particle if outside of screen
        end
    end
    
    for i, t in pairs(touches) do        -- add particles at touch position
        if t.state == MOVING or t.state == BEGAN then
            local rc = color(col.r, col.g, col.b, col.a)
            local v = 1.0 - varyBrightness * math.random()
            rc.r = rc.r * v
            rc.g = rc.g * v
            rc.b = rc.b * v
            table.insert( parts, {x=t.x, y=t.y, speedRnd=(gspeed + (gspeed/2)*math.random()), sizeRnd=(gsize + (gsize/2)*math.random(-1,1)), ftl=framesToLive, ftlCounter=framesToLive, c = rc, s = seed} )
        end
    end
end

.@KilamMalik, this is great. Is there a way to make the swirls follow the line direction (while you draw the line) and then shoot off, transversely, when you lift your finger?

Thanks for the explanation mpilgrem, much apreciated!

@KilamMalik
That looks great, thanks for posting your results. I hope you’ll continue working on this, it’s pretty cool!

Does anybody have an idea if codea supports additive blending?

I’m stealing this one for the Harmony program, it’s just fantastic. I’ve added a few tweaks to make the trails go “behind” the stroke.

Thanks andrew, Harmony is an awesome drawing tool. Looking forward to your results!

I,ve been experimenting with 3d line flows as the noise takes 3 coords. It’s kind of interesting, the resulting cabled vistas just scream to be flown through, or at least rotated, but frustratingly it’s just a flat drawing… :slight_smile:

I’ve now added this to the Harmony Code. I had to make a few changes. The most significant was to change the points to lines. This is because in the Harmony application I can’t keep the RETAINED backing mode as there’s too much going on on the screen. I tried drawing to an image, but the frame rate is so abysmally slow for that that I ditched it. So I fell back on meshes, but adding thousands of circles also became prohibitively slow. And so I fell back on line segments in meshes and finally got a decent frame rate from it.

As part of the implementation, I lost the organic way that the lines “grow”. I’m a bit sad about that as I really like that aspect of it. But it’s quicker to add each flow line in one go than to keep iterating back adding one more segment each time.

The “curl” is rotated to point (generally) back along the path. I’ve adjusted a few of the parameters a bit to make it feel a bit more like you are dragging the flow lines - not saying that I’ve gotten them right, though.

The Harmony project is at http://www.math.ntnu.no/~stacey/code/CodeaHarmony and you’ll also need my Library: http://www.math.ntnu.no/~stacey/code/CodeaLibrary.

I’ll post a screenshot once I’ve uploaded one.

I had some great fun playing with this, @Kurl ! Now to try the Andrew Stacey version.