Physical Brush

Experiment using the physics engine to model the individual bristles in a brush. Also picks up some of the ‘paint’ already on the paper.

-- Physical Brush
-- Herwig Van Marck
function setup()
    print("Physical Brush experiment")
    canvas=Canvas()
    iparameter("size",1,100,50)
    iparameter("r",0,255,225)
    iparameter("g",0,255,225)
    iparameter("b",0,255,225)
    iparameter("a",0,255,50)
end

function touched(touch)
    if (touch.state==BEGAN) then
        br=Brush(touch.x,touch.y,size,50,color(r,g,b,a))
    elseif (touch.state==MOVING) then
        br:move(touch.x,touch.y)
    elseif (touch.state==ENDED) then
        br:free()
        br=nil
    end
end

function draw()
    background(0, 0, 0, 255)
    strokeWidth(5)

    if (br) then
        canvas:paint(br)
    end
    
    canvas:draw()
    
    if (br) then
        br:draw()
    end
    
end

Bristle = class()

function Bristle:init(x,y,clr)
    -- you can accept and set parameters here
    self.body=physics.body(CIRCLE,4)
    self.body.x=x
    self.body.y=y
    self.body.interpolate=true
    self.body.sleepingAllowed=false
    self.body.gravityScale=0.1
    self.body.restitution=0.0
    self.body.density=100
    self.x=x
    self.y=y
    self.color=clr
end

function Bristle:free()
    self.body:destroy()
    self.body=nil
end

function Bristle:draw()
    -- Codea does not automatically call this method
    if (self.body) then
        pushStyle()
        fill(255, 0, 7, 185)
        stroke(0, 0, 0, 255)
        strokeWidth(1)
        ellipseMode(PROJECT)
        ellipse(self.body.position.x,self.body.position.y,self.body.radius*2)
        popStyle()
    end
end

function Bristle:paint(x,y,img)
    -- Codea does not automatically call this method
    if (self.body) then
        pushStyle()
        noFill()
        stroke(self.color)
        strokeWidth(15)
        --lineCapMode(SQUARE)
        noSmooth()
        local rl,gl,bl,al=img:get(self.body.position.x,self.body.position.y)
        line(self.x,self.y,self.body.position.x,self.body.position.y)
        self.x=self.body.position.x
        self.y=self.body.position.y
        local f=(255-al)/255.0
        self.color=color(self.color.r*0.95+r*0.05*f+rl*0.05*(1-f),
            self.color.g*0.95+g*0.05*f+gl*0.05*(1-f),
            self.color.b*0.95+b*0.05*f+bl*0.05*(1-f),
            self.color.a)
        popStyle()
    end
end

function Bristle:contact(x,y)
    if (self.body) then
        return self.body:testPoint(vec2(x,y))
    else
        return false
    end
end

Brush = class()

function Brush:init(x,y,size,nr,clr)
    self.x = x
    self.y = y
    self.size = size
    self.color=clr or color(255, 255, 255, 255)
    self.nr=0
    self.bristles={}
    local i=nr
    local try=15
    while (i>0) do
        local r=math.random(0,self.size - 5)
        local ang=math.random(0,99)*0.02*math.pi
        local xb=r*math.cos(ang)+self.x
        local yb=r*math.sin(ang)+self.y
        local flg=true
        for j,bristle in pairs(self.bristles) do
            if bristle:contact(xb,yb)==true then
                flg=false
                try = try - 1
                if try==0 then
                    try=15
                    i = i - 1
                end
                break
            end
        end
        if flg then
            local col=color(clamp(0,255,clr.r + math.random(-5,5)),
                clamp(0,255,clr.g + math.random(-5,5)),
                clamp(0,255,clr.b + math.random(-5,5)),
                clamp(0,255,clr.a + math.random(-5,5)))
            table.insert(self.bristles,Bristle(xb,yb,col))
            self.nr = self.nr + 1
            i = i - 1
        end
    end
    print(self.nr.." bristles")
end

function clamp(min,max,val)
    if val<min then return min end
    if val>max then return max end
    return val
end

function Brush:free()
    for i,bristle in pairs(self.bristles) do
        bristle:free()
    end
end

function Brush:move(x,y)
    self.x = x
    self.y = y
end

function Brush:draw()
    -- Codea does not automatically call this method
    pushStyle()
    fill(255, 255, 255, 40)
    stroke(0, 0, 0, 255)
    strokeWidth(1)
    ellipseMode(CENTER)
    ellipse(self.x,self.y,self.size*2)
    popStyle()
    pushStyle()
    stroke(0, 95, 255, 141)
    strokeWidth(2)
    local gain=2.0
    local damp=0.9
    for i,bristle in pairs(self.bristles) do
        local worldAnchor = bristle.body:getWorldPoint(vec2(0,0))
        local touchPoint = vec2(self.x,self.y)
        local diff = touchPoint - worldAnchor
        local vel = bristle.body:getLinearVelocityFromWorldPoint(worldAnchor)
        bristle.body:applyForce( (1/1) * diff * gain - vel * damp, worldAnchor)
        --line(self.x,self.y,worldAnchor.x,worldAnchor.y)
        --bristle:draw()
    end
    popStyle()
end

function Brush:paint(img)
    pushStyle()
    fill(self.color)
    stroke(0, 0, 0, 0)
    ellipseMode(CENTER)
    --ellipse(self.x,self.y,self.size)
    popStyle()
    for i,bristle in pairs(self.bristles) do
        bristle:paint(self.x,self.y,img)
    end
end

Canvas = class()

function Canvas:init()
    self.image = image(WIDTH,HEIGHT)
end

function Canvas:draw()
    pushStyle()
    spriteMode(CORNER)
    sprite(self.image,0,0)
    popStyle()
end

function Canvas:paint(brush)
    setContext(self.image)
    brush:paint(self.image)
    setContext()
end


Cool. I’ll try it when I can :slight_smile:

Very nice experiment, @Herwig! Really love the idea. I’m impressed at the way you modelled the bristles and have them mix with the underlying colours.

Just a note: painting off the edge of the screen can cause an error.

@Simeon Good point, I’ll have to test for the edge! Thanks!

By the way, @Herwig, did you take a look at @DaveSapien’s Paint simulation in Codea?

http://www.youtube.com/watch?v=h7k3Xz16qAM

Thanks @Simeon! I didn’t know. If @DaveSapien publishes his code I will definitely have a look !

Were is @DaveSapien’s code??? *-:slight_smile:

Hallp were is @DaveSapien’s code?

How come i can’t copy any code in the ipad? Do i have to do something?