Take the 2-minute tour ×
TeX - LaTeX Stack Exchange is a question and answer site for users of TeX, LaTeX, ConTeXt, and related typesetting systems. It's 100% free, no registration required.

How easy is the following animation made with TikZ, PSTricks, and Asymptote?

enter image description here

Use the following as a starting template to save your time.

\documentclass[border=12pt]{standalone}
\begin{document}
.
\end{document}
share|improve this question
    
A challenging problem that I'll look into after the thesis is out of the way. In the meantime, en.wikipedia.org/wiki/Collision_detection might help. –  Jubobs Jan 15 at 1:55
9  
+1 for the template –  Papiro Jan 15 at 6:49
    
One down vote detected. Thanks. –  cyanide-based food Jan 18 at 11:34

1 Answer 1

up vote 36 down vote accepted
+400

Not perfect, not optimal, not commented, not quick to compile, and not portable. Requires lualatex.

\documentclass[tikz,border=.125cm]{standalone}
\directlua{%

Vector = {}
Vector.__index = Vector

function Vector.new(x, y)
  local obj = {x=x, y=y}
  setmetatable(obj, Vector)
  return obj
end

function Vector.fromPolar(a, r)
  return Vector.new(math.cos(math.rad(a))*r, math.sin(math.rad(a))*r)
end

function Vector:copy()
  return Vector.new(self.x, self.y)
end

function Vector.__add(p, q)
  return Vector.new(p.x+q.x, p.y+q.y)
end

function Vector.__sub(p, q)
  return Vector.new(p.x-q.x, p.y-q.y)
end

function Vector.__mul(p, q)
  if getmetatable(p) == Vector then
    if getmetatable(q) == Vector then
      return p.x*q.x + p.y*q.y
    else
      return Vector.new(p.x*q, p.y*q)
    end
  else
    return Vector.new(p*q.x, p*q.y)
  end
end

function Vector.__div(p, q)
  return Vector.new(p.x/q, p.y/q)
end

function Vector.__unm(p)
  return Vector.new(-p.x, -p.y)
end

function Vector:length()
  return math.sqrt(self.x^2 +  self.y^2)
end

function Vector:angle()
  return math.atan2(self.y, self.x)
end

function Vector:normalised()
  local l
  l = self:length()
  if l > 0 then
    return self / l
  else
    return Vector.new(0,0)
  end
end

function Vector:tostring()
  return "(" .. self.x .. ", " .. self.y .. ")"
end



History = {}
History.__index = History

function History.new(l)
  local obj = {data={}, length=l, _i=1}
  setmetatable(obj, History)
  return obj
end

function History:update(v)
  self.data[self._i] = v
  self._i = self._i + 1
  if self._i > self.length then
    self._i = 1
  end
end

function History:get(i)
  local j
  j = self._i - i 
  if j < 1 then
    j = j + self.length
  end
  return self.data[j]
end

function History:tostring()
  local s, i
  s = self:get(1):tostring()
  for i = 2,self.length do
    s = s .. " -- " .. self:get(i):tostring()
  end 
  return s
end


Ball = {}
Ball.__index = Ball

function Ball.new(pos, dir, r, h, c)
  h = h or 1
  c = c or "black"
  local obj = {pos=pos, dir=dir, radius=r, history=History.new(h), color=c}
  setmetatable(obj, Ball)
  local i
  for i=1,h do
    obj.history:update(pos)
  end
  return obj
end

function Ball:move()
  self.history:update(self.pos)
  self.pos = self.pos + self.dir
end

function Ball:collide(b)
  local d = self.pos - b.pos
  return d:length() <= (self.radius + b.radius)
end

function Ball:tostring()
  return self.pos:tostring() .. " circle [radius=" .. self.radius .. "]"
end
Arena = {}
Arena.__index = Arena

function Arena.new(R, n, r, h, col, ncol)
  local obj = {radius=R, balls={}, nballs=n}
  setmetatable(obj, Arena)
  local i, j, c, ball, done
  for i = 1,n do
    done = false
    while not done do
      c = col[math.random(1, ncol)]
      ball = Ball.new(Vector.fromPolar(math.random(0,359), math.random()*(R-r)), 
        Vector.fromPolar(math.random(0,359), math.random()), r, h, c)
      done = true
      if i > 1 then
        for j = 1,i-1 do
          if ball:collide(obj.balls[j]) then
            done = false
            break
          end
        end
      end
    end
    obj.balls[i] = ball
  end
  return obj
end

function Arena:update(K)
  K = K or 1
  local i, j, k, b, d, n, v
  local v1, v2, vn1, vn2, vt1, vt2
  for k = 1,K do
    for i = 1,self.nballs do
      self.balls[i]:move()
      if self.balls[i].pos:length() >= (self.radius-self.balls[i].radius) then
        n = -self.balls[i].pos:normalised()
        d = self.balls[i].dir
        self.balls[i].dir = d - 2*(d*n)*n
        self.balls[i]:move()
      end
      for j = 1,self.nballs do
        if not (i == j) then
          if self.balls[i]:collide(self.balls[j]) then
            d = self.balls[i].pos - self.balls[j].pos
            v1 = self.balls[i].dir
            v2 = self.balls[j].dir
            n = d:normalised()
            vn1 = (v1*(-n))*(-n)
            vn2 = (v2*n)*n
            vt1 = vn1 - v1
            vt2 = vn2 - v2
            v1 = vt1 + vn2
            v2 = vt2 + vn1
            self.balls[i].dir = v1
            self.balls[j].dir = v2
            self.balls[i]:move()
            self.balls[j]:move()
          end
        end
      end
    end
  end
end
}

\def\getlua#1{\directlua{tex.print("" .. #1)}}

\pgfdeclarelayer{background}
\pgfsetlayers{background,main} 
\begin{document}
\directlua{%
A = Arena.new(75, 20, 5, 75, {"red", "yellow", "pink", "green", "orange", "purple", "blue"}, 7)
}

\foreach \j in {1,...,100}{

\begin{tikzpicture}[x=1pt,y=1pt]

\draw [very thick] circle [radius=\getlua{A.radius}];

\foreach \i in {1,...,\getlua{A.nballs}}{
% 
\fill [opacity=0.5] \getlua{A.balls[\i]:tostring()};
%
\begin{pgfonlayer}{background}
  \draw [very thick, line join=round, line cap=round, \getlua{A.balls[\i].color}] 
    \getlua{A.balls[\i].history:tostring()};
\end{pgfonlayer}
}

\directlua{A:update(5)}

\end{tikzpicture}
}
\end{document}

enter image description here

share|improve this answer
6  
I can watch this for 10 hours nonstop. :) –  Paulo Cereda Jan 17 at 20:49
    
@CodeMocker You mean you want the balls to move faster? So fast that you can't count the balls? –  Mark Wibrow Jan 18 at 7:53
1  
@CodeMocker actually there appears to be some problems with the code as sometimes balls get stuck together or to the edge of the circle. –  Mark Wibrow Jan 18 at 10:44
    
@MarkWibrow - Yes, the collision code in Arena:update doesn't look like standard Physics, and doesn't generate very realistic collisions. This may be the cause of the strange behaviour you observed. e.g. the collision between red and yellow at the 6'oclock position in frame 78 does not conserve momentum. –  Andrew Kepert Jan 20 at 7:52
    
@AndrewKepert possibly, but every frame of the gif corresponds to 5 iterations of the update method. Most of the problems (I believe) with collisions stem from the fact that the balls are updated in a loop, and both balls in a collision are moved without checking if this leads to further collisions (until the next update). Either way it should be straightforward for someone who is more familiar with the "standard Physics" to replace the code as the overloaded operators in Vector class mean the necessary equations can be specified easily. –  Mark Wibrow Jan 20 at 8:48

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.