Flexible words

By popular request (two people asked), here’s the code for my latest mesh experiments. What I’ve been trying to do is stick a bit of physics in to a mesh by defining some dynamical rules. If you look at the start of the code then there are a few options for different settings: what touching the screen means (method) and how the vertices of the mesh react to each other (the idea is to model them either as connected by springs - NEWTON (though HOOKE would be more appropriate) or as a rubber sheet (TENSION)). showpoints is quite fun.



displayMode(FULLSCREEN)
--supportedOrientations(LANDSCAPE_LEFT)
--startRecording()
--stopRecording()

DRAG = 1
REPELL = 2
ATTRACT = 3
NEWTON = 1
TENSION = 2

function setup()
    watch("debug")
    method = REPELL
    ode = TENSION
    --showpoints = true
    screen = mesh()
    side = 50
    spr = 1
    fr = .1
    ht = side*math.sqrt(3)/2
    local n = math.floor(WIDTH/side)+1
    local m = math.floor(HEIGHT/ht)+1
    local ad,v,rpts,pt
    local pts = {}
    tri = {}
    for i=1,m do
        rpts = {}
        ad = (1-i%2)*side/2
        for j = 1,n do
            v = vec2(ad + (j-1)*side,(i-1)*ht)
            pt = {point = v, velocity = vec2(0,0), neighbours = {}}
            
            if j ~= 1 then
                table.insert(pt.neighbours,rpts[j-1])
                table.insert(rpts[j-1].neighbours,pt)
            end
            if i ~= 1 then
                table.insert(pt.neighbours,pts[i-1][j])
                table.insert(pts[i-1][j].neighbours,pt)
                if i%2 == 1 then
                    if j ~= 1 then
                        table.insert(pt.neighbours,pts[i-1][j-1])
                        table.insert(pts[i-1][j-1].neighbours,pt)
                        table.insert(tri,{pt,rpts[j-1],pts[i-1][j-1]})
                        table.insert(tri,{pt,pts[i-1][j-1],pts[i-1][j]})
                    end
                else
                    if j ~= n then
                        table.insert(pt.neighbours,pts[i-1][j+1])
                        table.insert(pts[i-1][j+1].neighbours,pt)
                        table.insert(tri,{pt,pts[i-1][j],pts[i-1][j+1]})
                    end
                    if j ~= 1 then
                        table.insert(tri,{pt,rpts[j-1],pts[i-1][j]})
                    end
                end
            end
            table.insert(rpts,pt)
        end
        table.insert(pts,rpts)
    end
    points = {}
    for k,v in ipairs(pts) do
        for l,u in ipairs(v) do
            table.insert(points,u)
        end
    end
    local n
    for k,v in ipairs(points) do
        n = 0
        for l,u in ipairs(v.neighbours) do
            n = n + 1
        end
        if n == 6 then
            v.fixed = false
        else
            v.fixed = true
        end
    end
    img = image(WIDTH,HEIGHT)
    pushStyle()
    setContext(img)
    fill(0, 107, 255, 255)
    noSmooth()
    rect(0,0,WIDTH,HEIGHT)
    font("Noteworthy-Bold")
    fontSize(160)
    fill(255, 255, 255, 255)
    local fm = fontMetrics()
    local h = 1.8*fm.xHeight
    fill(0, 255, 21, 255)
    text("Words",WIDTH/2,HEIGHT/2 + h)
    text("are",WIDTH/2,HEIGHT/2)
    text("flexible",WIDTH/2,HEIGHT/2 - h)
    setContext()
    popStyle()
    screen.texture = img
    texc = {}
    for k,v in ipairs(tri) do
        for l,u in ipairs(v) do
            table.insert(texc,vec2(u.point.x/WIDTH,u.point.y/HEIGHT))
        end
    end
    screen.texCoords = texc
    --[[
    screen = mesh()
    local a = {}
    local b = {}
    local c = {}
    a.point = vec2(WIDTH/2,HEIGHT/2) + vec2(0,200)
    b.point = vec2(WIDTH/2,HEIGHT/2) + vec2(-200,0)
    c.point = vec2(WIDTH/2,HEIGHT/2) + vec2(200,0)
    a.velocity = vec2(0,0)
    b.velocity = vec2(0,0)
    c.velocity = vec2(0,0)
    a.neighbours = {b,c}
    b.neighbours = {a,c}
    c.neighbours = {b,a}
    a.fixed = false
    b.fixed = false
    c.fixed = false
    tri = {{a,b,c}}
    points = {a,b,c}
    --]]
end

Code continues:

function draw()
    background(0, 0, 0, 255)
    if ode == NEWTON then
    local f,c,n,cl
    for k,v in ipairs(points) do
        if method == DRAG then
        if v == touchedpt then
            f = touchpt - v.point
        else
            f = vec2(0,0)
        end
        elseif method == REPELL then
        if touchpt then
        f = v.point - touchpt
        if f:len() > 1 then
            f = 100000*f/(f:len()^3)
        end
        else
            f = vec2(0,0)
        end
        elseif method == ATTRACT then
            if touchpt then
        f = v.point - touchpt
        if f:len() > 1 then
            f = -5000*f/(f:len()^3)
        end
        else
            f = vec2(0,0)
        end
        end
        n = 0
        cl = 0
        for l,u in ipairs(v.neighbours) do
            c = u.point - v.point
            n = n + 1
            f = f + spr*c
            cl = cl + c:len()
        end
        if n == 6 then -- interior point
            f = f -  fr*v.velocity
            v.velocity = v.velocity + DeltaTime * f
        end
        cl = cl/n - side
        v.colour = 64*(math.atan(cl)/math.pi+1/2) + 191
    end
    for k,v in ipairs(points) do
        v.point = v.point + DeltaTime*v.velocity
    end
    else
        for k,v in ipairs(points) do
            v.force = vec2(0,0)
        end
        local a,b,c
        for k,v in ipairs(tri) do
            a = v[3].point - v[2].point
            b = v[1].point - v[3].point
            c = v[2].point - v[1].point
            a,b,c = TriangleForce(a,b,c,side)
            v[1].force = v[1].force + a
            v[2].force = v[2].force + b
            v[3].force = v[3].force + c
        end
        for k,v in ipairs(points) do
            
            if not v.fixed then
                if method == DRAG then
                    if v == touchedpt then
                        v.force = v.force + (touchpt - v.point)
                    end
                elseif method == REPELL then
                    if touchpt then
                        local f = v.point - touchpt
                        if f:len() > side then
                            f = 100000*f/(f:len()^3)
                            v.force = v.force + f
                        end
                        
                    end
                elseif method == ATTRACT then
                    if touchpt then
                        local f = v.point - touchpt
                        if f:len() > 1 then
                            f = -100000*f/(f:len()^3)
                        end
                        v.force = v.force + f
                    end
                end
                v.force = v.force - fr*v.velocity
                if showpoints then
                
                    end
                v.velocity = v.velocity + DeltaTime*v.force
                --v.velocity =  DeltaTime*v.force
                v.point = v.point + DeltaTime*v.velocity
                
            end
        
        end
    end
    local n,c,cl
    for k,v in ipairs(points) do
        n = 0
        cl = 0
        for l,u in ipairs(v.neighbours) do
            c = u.point - v.point
            n = n + 1
            cl = cl + c:len()
        end
        cl = cl/n - side
        v.colour = 128*(math.atan(cl)/math.pi+1/2) + 127
    end
    fill(0, 255, 38, 255)
    local ver = {}
    local col = {}
    for k,v in ipairs(tri) do
        for l,u in ipairs(v) do
            table.insert(ver,u.point)
            table.insert(col,color(255,255,255,u.colour))
        end
    end
    screen.vertices = ver
    screen.colors = col
    screen:draw()
    if showpoints then
    ellipseMode(RADIUS)
    
    fill(255, 0, 0, 255)
    for k,v in ipairs(points) do
        noStroke()
        ellipse(v.point.x, v.point.y,2)
        strokeWidth(5)
                stroke(255, 0, 0, 255)
                line(v.point.x,v.point.y,
                    v.point.x+v.force.x,v.point.y+v.force.y)
    end
    fill(230, 255, 0, 135)
    end
    if showtouch then
    if touchpt then
        ellipse(touchpt.x,touchpt.y,50)
    end
    end
end
            
function touched(touch)
    touchpt = vec2(touch.x,touch.y)
    if touch.state == BEGAN then
        if method == DRAG then
            table.sort(points, function(a,b) return touchpt:distSqr(a.point) < touchpt:distSqr(b.point) end)
            touchedpt = points[1]
        end
    elseif touch.state == ENDED then
        touchedpt = nil
        touchpt = nil
    end
end

function TriangleForce(a,b,c,s)
    -- force exerted on the vertices by a deformed triangle
    -- first approximation: drive each point to make an isosceles
    -- triangle of the same area with the other side fixed
    local vv,l,h
    local iarea = s^2 * math.sqrt(3)/4
    
    local area = a:rotate90():dot(b)/2
    local sides = {a,b,c}
    
    local r = {}
    for k,v in ipairs(sides) do
        l = v:len()
        h = iarea*2/l
        vv = v:rotate90()/l
        vv = h*Sign(vv:dot(sides[k%3+1]))*vv
        table.insert(r, (vv + .5*v + sides[(k+1)%3+1]))
    end
    return unpack(r)
end

function Sign(x)
    if x > 0 then
        return 1
    elseif x < 0 then
        return 1
    else
        return 0
    end
end

function TriangleArea(a,b,c)
    return math.abs(
        a:dot(b:rotate90()) 
        + b:dot(c:rotate90()) 
        + c:dot(a:rotate90())
        )/2
end

Cool! I’ll try it when I get a chance!

Thanks Andrew! I saw your YouTube video a day or two ago and was very impressed. Nice to have the code to see how you did it.

I’m really glad that I posted this code! Through my own stupidity, I just lost a load of code and this was the main bit - notlostforever, thankfully!

I tend to use code sharing as a form of backup as well.

Got a chance to try this out. But, is it just me or is there something wrong with the code? Because I got the text upside down. On iPad 1, btw.

Thanks for the code, Andrew. Really appreciate it. :slight_smile:

Oh, Andrew… Would you mind sharing the code of the other program (text on a sphere)? I’m curious how you did that. Thank you in advance. :slight_smile:

@bee Ah, the text-upside-down thing is due to a “feature” in how the image is rendered on to the mesh which meant that it got turned upside down (basically, the coordinates for the texture start at top-left instead of bottom-left like everything else in Codea). This is fixed in 1.3.1. As I’m a beta-user, I’m using the fixed version. I forgot about that, sorry.

Unfortunately, the text-on-a-sphere was part of the code I lost. I should be able to reconstruct it (I only lost that particular part of it), but I’m currently having problems with the project that that is part of so it might take me a few days.

Thank you, Andrew. Feel sorry for your lost. Looking forward for your code. I’m neither mathematician or visual programmer, I learn a lot from your code, also from other experts here. :slight_smile:

In fact, I thought you might do the upside-down text intentionally. However,it looks good when I saw from the mirror. @Andrew_Stacey Thank you.

To flip the image the right way up (for non beta-testers), locate the section that looks like:

    screen.texture = img
    texc = {}
    for k,v in ipairs(tri) do
        for l,u in ipairs(v) do
            table.insert(texc,vec2(u.point.x/WIDTH,u.point.y/HEIGHT))
        end
    end
    screen.texCoords = texc

(I think it’s around line 107) and change u.point.y/HEIGHT to 1 - u.point.y/HEIGHT. That ought to do it.

@Andrew Thank you. I got it works now.

@Andrew_Stacey: Pardon me for being a newbie in this mesh thingy. Would you please explain what is your code doing here? I mean in a bit detail explanation. What I have understood is that you draw an image which contains some text, devide it into triangles which become meshes, then apply some physics to it where touch happens. Is it correct? What I haven’t yet understood is how the animation become so smooth. For example, the text isn’t just got cropped by the triangles. Instead, it seems so fluid as if the text is over some water. I know it’s done somewhere in the complicated computation code, but in which part to be exact? Could you please explain how the computation is done to create such effect? For any kind of explanation, I thank you in advance. If this request bothers you, feel free to ignore it. :slight_smile:

@bee: It doesn’t bother me at all. I like explaining stuff! I’ll have a think about how best to go through it - I had been thinking of revisiting this and changing my manual physics stuff for the inbuilt physics engine (now that I’ve had a play with that and at least learnt the basics).

@Andrew_Stacey: No, please don’t use the built-in physic. I’d like to know how you did such beautiful effect using computation. I want to learn the beauty of the mathematics behind it. I always love beautiful representation of mathematics! Though I’m not always able to understand it. :smiley: