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