/ animation / animate.py
animate.py
  1  """
  2  DreamTalk Fluent Animation API
  3  
  4  Provides the .animate property pattern inspired by Manim:
  5  
  6      # Animate a single parameter to a value
  7      self.play(virus.animate.fold(0.5), run_time=1)
  8  
  9      # Chain multiple parameter animations
 10      self.play(
 11          virus.animate.fold(0.5).scale(2),
 12          run_time=1.5
 13      )
 14  
 15      # Animate through a sequence of values
 16      self.play(virus.animate.fold.sequence(1, 0.1, 1), run_time=2)
 17  
 18      # Animate position components
 19      self.play(virus.animate.x(100).y(50), run_time=1)
 20  
 21  The fluent API makes animations more readable and composable.
 22  It generates the same ScalarAnimation/VectorAnimation objects
 23  under the hood, but with cleaner syntax.
 24  """
 25  
 26  import c4d
 27  from DreamTalk.animation.animation import ScalarAnimation, VectorAnimation, AnimationGroup
 28  
 29  
 30  class AnimatorProxy:
 31      """
 32      Proxy object returned by holon.animate
 33  
 34      Provides attribute access that returns ParameterAnimator objects
 35      for fluent animation building.
 36  
 37      Usage:
 38          virus.animate.fold(0.5)  # Returns animation for fold parameter
 39          virus.animate.x(100)     # Returns animation for x position
 40      """
 41  
 42      def __init__(self, target):
 43          self.target = target
 44          self._animations = []
 45  
 46      def __getattr__(self, name):
 47          """Return a ParameterAnimator for the named parameter."""
 48          # Avoid recursion on special attributes
 49          if name.startswith('_'):
 50              raise AttributeError(name)
 51          # Don't intercept attributes that Scene.get_animation checks for
 52          if name in ('scalar_animations', 'execute'):
 53              raise AttributeError(name)
 54          return ParameterAnimator(self.target, name, self)
 55  
 56      def _add_animation(self, animation):
 57          """Add an animation to the chain."""
 58          self._animations.append(animation)
 59  
 60      @property
 61      def animations(self):
 62          """
 63          Return all chained animations as an AnimationGroup.
 64  
 65          This property is used by Scene.get_animation() to extract
 66          the animations for playback.
 67          """
 68          if len(self._animations) == 0:
 69              return AnimationGroup()
 70          if len(self._animations) == 1:
 71              return self._animations[0]
 72          return AnimationGroup(*self._animations)
 73  
 74  
 75  class ParameterAnimator:
 76      """
 77      Animator for a specific parameter.
 78  
 79      Can be called to animate to a value, or accessed for .sequence()
 80      to animate through multiple values.
 81  
 82      Usage:
 83          virus.animate.fold(0.5)           # Animate fold to 0.5
 84          virus.animate.fold.sequence(1, 0) # Animate fold through 1, then 0
 85      """
 86  
 87      def __init__(self, target, param_name, proxy=None):
 88          self.target = target
 89          self.param_name = param_name
 90          self.proxy = proxy
 91  
 92      def __call__(self, value, relative=False):
 93          """
 94          Animate the parameter to a single value.
 95  
 96          Args:
 97              value: Target value to animate to
 98              relative: If True, add to current value instead of setting absolute
 99  
100          Returns:
101              AnimatorProxy for chaining, or ScalarAnimation if not chaining
102          """
103          desc_id = self._get_desc_id()
104          if desc_id is None:
105              raise ValueError(f"Parameter '{self.param_name}' not found on {self.target}")
106  
107          animation = ScalarAnimation(
108              target=self.target,
109              descriptor=desc_id,
110              value_fin=value,
111              relative=relative
112          )
113  
114          # Update the actual value on the object
115          self.target.obj[desc_id] = value
116  
117          if self.proxy:
118              self.proxy._add_animation(animation)
119              return self.proxy
120          return animation
121  
122      def sequence(self, *values):
123          """
124          Animate through a sequence of values.
125  
126          Each value gets an equal portion of the total run_time.
127  
128          Args:
129              *values: Values to animate through in order
130  
131          Returns:
132              AnimationGroup containing the sequenced animations
133  
134          Example:
135              virus.animate.fold.sequence(1, 0.1, 1)  # Open, close, open
136          """
137          desc_id = self._get_desc_id()
138          if desc_id is None:
139              raise ValueError(f"Parameter '{self.param_name}' not found on {self.target}")
140  
141          animations = []
142          n = len(values)
143  
144          for i, value in enumerate(values):
145              anim = ScalarAnimation(
146                  target=self.target,
147                  descriptor=desc_id,
148                  value_fin=value,
149                  rel_start=i / n,
150                  rel_stop=(i + 1) / n
151              )
152              animations.append(anim)
153  
154          # Set final value on object
155          if values:
156              self.target.obj[desc_id] = values[-1]
157  
158          animation_group = AnimationGroup(*animations)
159  
160          if self.proxy:
161              self.proxy._add_animation(animation_group)
162              return self.proxy
163          return animation_group
164  
165      def _get_desc_id(self):
166          """
167          Get the C4D DescID for this parameter.
168  
169          Checks in order:
170          1. UserData parameter with matching name (e.g., fold_parameter)
171          2. Built-in position/rotation/scale components (x, y, z, h, p, b)
172          3. UserData by searching the container
173          """
174          # Check for parameter object attribute (e.g., self.fold_parameter)
175          param_attr = self.param_name + '_parameter'
176          if hasattr(self.target, param_attr):
177              param = getattr(self.target, param_attr)
178              if hasattr(param, 'desc_id'):
179                  return param.desc_id
180  
181          # Check for direct desc_id attribute (e.g., self.fold_id)
182          id_attr = self.param_name + '_id'
183          if hasattr(self.target, id_attr):
184              return getattr(self.target, id_attr)
185  
186          # Handle built-in position components
187          position_map = {
188              'x': (c4d.ID_BASEOBJECT_POSITION, c4d.VECTOR_X),
189              'y': (c4d.ID_BASEOBJECT_POSITION, c4d.VECTOR_Y),
190              'z': (c4d.ID_BASEOBJECT_POSITION, c4d.VECTOR_Z),
191          }
192          if self.param_name in position_map:
193              base, component = position_map[self.param_name]
194              return c4d.DescID(
195                  c4d.DescLevel(base, c4d.DTYPE_VECTOR, 0),
196                  c4d.DescLevel(component, c4d.DTYPE_REAL, 0)
197              )
198  
199          # Handle built-in rotation components
200          rotation_map = {
201              'h': (c4d.ID_BASEOBJECT_ROTATION, c4d.VECTOR_X),
202              'p': (c4d.ID_BASEOBJECT_ROTATION, c4d.VECTOR_Y),
203              'b': (c4d.ID_BASEOBJECT_ROTATION, c4d.VECTOR_Z),
204          }
205          if self.param_name in rotation_map:
206              base, component = rotation_map[self.param_name]
207              return c4d.DescID(
208                  c4d.DescLevel(base, c4d.DTYPE_VECTOR, 0),
209                  c4d.DescLevel(component, c4d.DTYPE_REAL, 0)
210              )
211  
212          # Handle built-in scale components
213          scale_map = {
214              'scale_x': (c4d.ID_BASEOBJECT_SCALE, c4d.VECTOR_X),
215              'scale_y': (c4d.ID_BASEOBJECT_SCALE, c4d.VECTOR_Y),
216              'scale_z': (c4d.ID_BASEOBJECT_SCALE, c4d.VECTOR_Z),
217          }
218          if self.param_name in scale_map:
219              base, component = scale_map[self.param_name]
220              return c4d.DescID(
221                  c4d.DescLevel(base, c4d.DTYPE_VECTOR, 0),
222                  c4d.DescLevel(component, c4d.DTYPE_REAL, 0)
223              )
224  
225          # Handle uniform scale
226          if self.param_name == 'scale':
227              # For uniform scale, we'd need to return multiple animations
228              # For now, just use scale_x as proxy
229              return c4d.DescID(
230                  c4d.DescLevel(c4d.ID_BASEOBJECT_SCALE, c4d.DTYPE_VECTOR, 0),
231                  c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)
232              )
233  
234          # Search UserData by name
235          if hasattr(self.target, 'obj'):
236              ud = self.target.obj.GetUserDataContainer()
237              for desc_id, bc in ud:
238                  if bc[c4d.DESC_NAME] == self.param_name:
239                      return desc_id
240  
241          return None
242  
243  
244  class VectorAnimatorProxy:
245      """
246      Proxy for animating vector parameters (position, rotation, scale).
247  
248      Usage:
249          virus.animate.position(100, 50, 0)
250          virus.animate.rotation(h=PI/4)
251      """
252  
253      def __init__(self, target, vector_type):
254          self.target = target
255          self.vector_type = vector_type  # 'position', 'rotation', 'scale'
256  
257      def __call__(self, x=None, y=None, z=None, vector=None, relative=False):
258          """Animate the vector to target values."""
259          if vector is not None:
260              if isinstance(vector, c4d.Vector):
261                  x, y, z = vector.x, vector.y, vector.z
262              else:
263                  x, y, z = vector
264  
265          descriptors = {
266              'position': c4d.ID_BASEOBJECT_POSITION,
267              'rotation': c4d.ID_BASEOBJECT_ROTATION,
268              'scale': c4d.ID_BASEOBJECT_SCALE,
269          }
270  
271          descriptor = descriptors.get(self.vector_type)
272          if descriptor is None:
273              raise ValueError(f"Unknown vector type: {self.vector_type}")
274  
275          # Get current values for any unspecified components
276          current = self.target.obj[descriptor]
277          if x is None:
278              x = current.x
279          if y is None:
280              y = current.y
281          if z is None:
282              z = current.z
283  
284          target_vector = c4d.Vector(x, y, z)
285  
286          animation = VectorAnimation(
287              target=self.target,
288              descriptor=descriptor,
289              vector=target_vector,
290              relative=relative
291          )
292  
293          self.target.obj[descriptor] = target_vector
294  
295          return animation