# Physics based Wordcloud

Implementation of a physics based Wordcloud. For the word count I used a Luna script (because it can ask the user for an input string, where one can use paste), resulting in a table in the data tab. Words are laid out next to each other until the screen is full. Then the physics engine is turned on to let gravity do its thing. Tapping on a word allows you to drag it to another position. Tapping anywhere else flips the gravity direction.

The most difficult part was how to get the outline of the words. Also, the setup is so computationally intensive that it needs to be done outside of the setup.

``````-- Wordcloud
-- Herwig Van Marck
-- Use this function to perform your initial setup
function setup()
print("Wordcloud\
Tap on a word to move it\
Tap anywhere else to flip gravity")
iparameter("showOutlines",0,1,0)
wall=physics.body(CHAIN,true,vec2(0,0),vec2(WIDTH,0),vec2(WIDTH,HEIGHT),vec2(0,HEIGHT))
wcl=WordCloud(tbl)
end

function touched(touch)
if wcl:touched(touch)==false then
if (touch.state==BEGAN) then
physics.gravity(-physics.gravity())
end
end
end

function drawObj(body)
pushMatrix()
translate(body.x, body.y)
rotate(body.angle)

if body.type == STATIC then
stroke(255,255,255,255)
elseif body.type == DYNAMIC then
stroke(150,255,150,255)
elseif body.type == KINEMATIC then
stroke(150,150,255,255)
end

if body.shapeType == POLYGON then
strokeWidth(5.0)
local points = body.points
for j = 1,#points do
a = points[j]
b = points[(j % #points)+1]
line(a.x, a.y, b.x, b.y)
end
elseif body.shapeType == CHAIN or body.shapeType == EDGE then
strokeWidth(5.0)
local points = body.points
local range = #points - 1
if (true) then
range = #points
end
for j = 1, range do
a = points[j]
b = points[j % #points +1]
line(a.x, a.y, b.x, b.y)
end
elseif body.shapeType == CIRCLE then
strokeWidth(5.0)
strokeWidth(2.5)
end

popMatrix()
end

function draw()
background(0, 0, 0, 255)
wcl:draw()
drawObj(wall)
end

tbl={}
tbl["codea"]=5
tbl["anything"]=1
tbl["lets"]=1
tbl["create"]=1
tbl["games"]=1
tbl["simulations"]=1
tbl["any"]=1
tbl["idea"]=1
tbl["have"]=1
tbl["turn"]=1
tbl["thoughts"]=1
tbl["into"]=1
tbl["interactive"]=1
tbl["creations"]=1
tbl["make"]=2
tbl["of"]=1
tbl["features"]=1
tbl["like"]=1
tbl["multi-touch"]=1
tbl["accelerometer"]=1
tbl["we"]=1
tbl["think"]=1
tbl["most"]=1
tbl["beautiful"]=1
tbl["editor"]=1
tbl["ll"]=1
tbl["use"]=2
tbl["s"]=1
tbl["easy"]=1
tbl["designed"]=1
tbl["touch"]=1
tbl["your"]=3
tbl["code"]=2
tbl["to"]=2
tbl["change"]=1
tbl["number"]=1
tbl["just"]=2
tbl["tap"]=1
tbl["and"]=4
tbl["drag"]=1
tbl["it"]=2
tbl["how"]=1
tbl["color"]=1
tbl["you"]=6
tbl["or"]=2
tbl["an"]=1
tbl["image"]=1
tbl["tapping"]=1
tbl["will"]=1
tbl["bring"]=1
tbl["up"]=1
tbl["visual"]=2
tbl["editors"]=1
tbl["let"]=2
tbl["choose"]=1
tbl["exactly"]=1
tbl["what"]=1
tbl["want"]=2
tbl["is"]=3
tbl["built"]=1
tbl["lua"]=1
tbl["programming"]=1
tbl["simple"]=1
tbl["elegant"]=1
tbl["language"]=2
tbl["that"]=3
tbl["doesn"]=1
tbl["t"]=1
tbl["rely"]=1
tbl["too"]=1
tbl["much"]=1
tbl["on"]=3
tbl["symbols"]=1
tbl["a"]=4
tbl["perfect"]=1
tbl["match"]=1
tbl["for"]=2
tbl["join"]=1
tbl["the"]=4
tbl["forums"]=1

PhysicsWord = class()

function PhysicsWord:init(str,x,y,fnt,fsize)
self.str=str
self.fnt=fnt
self.fsize=fsize
self.bounce=nil
local xofs=0;
local tblb={}
local tblt={}
pushStyle()
font(self.fnt)
fontSize(self.fsize)

for i=1,string.len(self.str) do
local chr=string.sub(self.str,i,i)
local xmin,ymin,xmax,ymax=self:boundingBox(chr)
table.insert(tblb,vec2(xofs+xmin,ymin))
table.insert(tblb,vec2(xofs+xmax,ymin))
table.insert(tblt,1,vec2(xofs+xmin,ymax))
table.insert(tblt,1,vec2(xofs+xmax,ymax))
xofs = xofs + textSize(chr)
end
for i=1,#tblt do
table.insert(tblb,tblt[i])
end
popStyle()
self.word = physics.body(POLYGON,unpack(tblb))
self.word.x=x
self.word.y=y
self.word.restitution=0.60
self.word.gravityScale=0.2
self.word.sleepingAllowed=false
end

function PhysicsWord:boundingBox(chr)
pushMatrix()
pushStyle()
font(self.fnt)
fontSize(self.fsize)
local w,h=textSize(chr)
local img=image(w,h)
setContext(img)
textMode(CORNER)
fill(255, 255, 255, 255)
text(chr,0,0)
setContext()
local xmin,ymin,xmax,ymax=w,h,0,0
for x=1,w do
for y=1,h do
local r,g,b,a=img:get(x,y)
if (a>0) then
if (xmin>=x) then
xmin=x
end
if (xmax<=x) then
xmax=x
end
if (ymin>=y) then
ymin=y
end
if (ymax<=y) then
ymax=y
end
end
end
end
--workaround for text bug
setContext(img)
textMode(CORNER)
setContext()

popStyle()
popMatrix()
return xmin,ymin,xmax,ymax
end

function PhysicsWord:draw()
local body=self.word
pushMatrix()
pushStyle()
translate(body.x, body.y)
rotate(body.angle)

stroke(150,255,150,255)
strokeWidth(5.0)
if (showOutlines==1) then
local points = body.points
for j = 1,#points do
a = points[j]
b = points[(j % #points)+1]
line(a.x, a.y, b.x, b.y)
end
end
fontSize(self.fsize)
font(self.fnt)
textMode(CORNER)
fill(255, 255, 255, 255)
text(self.str,0,0)
popStyle()
popMatrix()
if (nil) then -- waiting for inertia support
if (math.abs(body.angle)>80) then
if (self.bounce==nil) then
print(body.mass)
body:applyTorque(-120*body.angularVelocity*body.restitution*body.inertia)
self.bounce=true
end
else
self.bounce=nil
end
end
end

function PhysicsWord:touched(touch)
local touchPoint=vec2(touch.x,touch.y)
return self.word:testPoint(touchPoint)
end

``````

Wordcloud class:

``````WordCloud = class()

function WordCloud:init(tbl)
self.words = {}
self.touchMap = {}
self.strs = {}
self.sizes = {}
self.x = 5
self.y = 5
self.maxy = 0
self.done = nil
for str,sz in pairs(tbl) do
table.insert(self.strs,1,str)
table.insert(self.sizes,1,sz)
end
physics.pause()
end

-- adding the words in setup is to time consuming (does not work)
if not(self.done) then
pushStyle()
font("Arial-BoldMT")

fill(49, 127, 225, 255)
local str,sz = table.remove(self.strs),table.remove(self.sizes)
fontSize(16+16*sz)
local w,h=textSize(str)
if (self.x + w > WIDTH -10) then
self.x = 0
self.y = self.y + self.maxy
self.maxy = 0
end
if (self.maxy<h) then
self.maxy=h
end
if (self.y + h > HEIGHT -10) then
self.done=true
physics.resume()
else
local word=PhysicsWord(str,self.x,self.y,
"Arial-BoldMT",fontSize())
table.insert(self.words,word)
self.x = self.x + w
end

popStyle()
end
end

function WordCloud:draw()
local gain = 2.0
local damp = 0.5
for k,v in pairs(self.touchMap) do
local worldAnchor = v.word.word:getWorldPoint(v.anchor)
local touchPoint = v.tp
local diff = touchPoint - worldAnchor
local vel = v.word.word:getLinearVelocityFromWorldPoint(worldAnchor)
v.word.word:applyForce( (1/1) * diff * gain - vel * damp, worldAnchor)

line(v.tp.x, v.tp.y, worldAnchor.x, worldAnchor.y)
end

for i,word in ipairs(self.words) do
word:draw()
end
end

function WordCloud:touched(touch)
local touchPoint=vec2(touch.x,touch.y)
if touch.state == BEGAN then
for i,word in ipairs(self.words) do
if word:touched(touch) then
self.touchMap[touch.id] =
{tp = touchPoint, word = word, anchor = word.word:getLocalPoint(touchPoint)}
return true
end
end
elseif touch.state == MOVING and self.touchMap[touch.id] then
self.touchMap[touch.id].tp = touchPoint
return true
elseif touch.state == ENDED and self.touchMap[touch.id] then
self.touchMap[touch.id] = nil
return true;
end
return false
end
``````

Wow! I love it. It looks funny when all words fall. Fantastic job! Thank yor for sharing the code.
It also can float like cloud.

That’s a fantastic experiment Herwig. I’m really impressed that you managed to generate polygons for the word shapes — did you compute the bounding box of each letter then merge them using an algorithm?

I hope you don’t mind but I uploaded a video of the project so others can see:

Thanks @Simeon! The bounding box of each letter is calculated (using setContext(img) ) and then concatenated into a physics body of type polygon. If you set showOutlines to 1 you can see the actual polygons.

I had to dial down the gravity a bit, otherwise the polygons tended to crash together, fixing them to one another.

Also thanks for the video! I tried to do one myself, but for some reason the physics didn’t seem to work (polygons started overlapping?).

boundingBox == genius