Add Trapcode Particular VFX systems: Aux trails, Layer emitter, Bounce physics, Curves over life (v1.1.0)
e14f09d verified Download tests/test_trapcode_features.py from ArifiIslam/particri: direct link, hf CLI and curl.
- Browser
- Download file 4.43 kB
-
https://huggingface.co/ArifiIslam/particri/resolve/main/tests/test_trapcode_features.py
- Command line
-
hf download hf://ArifiIslam/particri/tests/test_trapcode_features.py
-
curl -L -o test_trapcode_features.py https://huggingface.co/ArifiIslam/particri/resolve/main/tests/test_trapcode_features.py
4.43 kB
| import pytest | |
| import numpy as np | |
| import particri | |
| def test_curves_over_life(): | |
| """Verify that all curve types can be assigned and evaluated during rendering.""" | |
| curves = [ | |
| particri.CurveType.Linear, | |
| particri.CurveType.Bell, | |
| particri.CurveType.EaseIn, | |
| particri.CurveType.EaseOut, | |
| particri.CurveType.GrowAndFade, | |
| particri.CurveType.Pulse, | |
| particri.CurveType.Decay, | |
| ] | |
| for curve in curves: | |
| engine = particri.ParticleEngine(max_particles=500, width=320, height=240) | |
| engine.add_emitter( | |
| shape=particri.EmitterShape.Point, | |
| position=(160.0, 120.0), | |
| rate=100.0, | |
| size_curve=curve, | |
| opacity_curve=curve, | |
| ) | |
| engine.burst(0, 20) | |
| engine.update(0.1) | |
| img = engine.render(bloom=False) | |
| assert img.shape == (240, 320, 4) | |
| assert engine.active_count >= 20 | |
| def test_aux_particle_system(): | |
| """Verify that parent particles emit secondary aux particles and death bursts.""" | |
| engine = particri.ParticleEngine(max_particles=5000, width=640, height=480) | |
| # Add a single parent emitter that bursts 5 particles | |
| engine.add_emitter( | |
| shape=particri.EmitterShape.Point, | |
| position=(320.0, 240.0), | |
| rate=0.0, # No continuous primary emission | |
| speed_range=(50.0, 50.0), | |
| lifetime_range=(0.5, 0.5), # Short lifetime | |
| ) | |
| # Enable Aux system with rate and death burst | |
| engine.enable_aux_system( | |
| rate=20.0, | |
| lifetime=1.0, | |
| size_range=(4.0, 1.0), | |
| spread=10.0, | |
| particle_shape=particri.ParticleShape.Glitter, | |
| emit_at_death=5, | |
| ) | |
| engine.burst(0, 5) | |
| assert engine.active_count == 5 | |
| # Advance physics: parent particles should emit aux particles along their path | |
| engine.update(0.2) | |
| assert engine.active_count > 5, "Aux system should have spawned child particles" | |
| # Advance past parent lifetime (0.5s): parent particles die, triggering emit_at_death bursts | |
| count_before = engine.active_count | |
| engine.update(0.4) | |
| # Child particles are still alive | |
| assert engine.active_count > 0 | |
| # Test disabling aux system | |
| engine.disable_aux_system() | |
| engine.clear() | |
| assert engine.active_count == 0 | |
| def test_bounce_plane_physics(): | |
| """Verify that particles falling downwards bounce off a floor plane.""" | |
| engine = particri.ParticleEngine(max_particles=100, width=640, height=480) | |
| floor_y = 400.0 | |
| engine.add_bounce_plane(coord=floor_y, bounce=0.6, friction=0.9, is_horizontal=True, is_max=True) | |
| # Emit a particle directly downward towards the floor | |
| engine.add_emitter( | |
| shape=particri.EmitterShape.Point, | |
| position=(320.0, 380.0), | |
| rate=0.0, | |
| speed_range=(100.0, 100.0), | |
| angle_range=(1.5707963, 1.5707963), # Exactly downwards (pi/2) | |
| lifetime_range=(2.0, 2.0), | |
| ) | |
| engine.burst(0, 1) | |
| # Update enough for particle to hit floor_y (20px at 100px/s takes 0.2s) | |
| engine.update(0.3) | |
| img = engine.render(bloom=False) | |
| assert img.shape == (480, 640, 4) | |
| assert engine.active_count == 1 | |
| def test_layer_mask_emitter(): | |
| """Verify that particles are spawned exclusively from non-empty pixels of an image mask.""" | |
| engine = particri.ParticleEngine(max_particles=2000, width=200, height=200) | |
| # Create a 200x200 mask with a 20x20 bright square in the upper-left (from x=10..30, y=10..30) | |
| mask = np.zeros((200, 200, 4), dtype=np.float32) | |
| mask[10:30, 10:30, 0] = 1.0 # Red | |
| mask[10:30, 10:30, 1] = 0.5 # Green | |
| mask[10:30, 10:30, 2] = 0.0 # Blue | |
| mask[10:30, 10:30, 3] = 1.0 # Alpha | |
| emitter_idx = engine.add_layer_emitter( | |
| image=mask, | |
| rate=0.0, | |
| speed_range=(0.0, 0.0), # Stationary particles | |
| lifetime_range=(5.0, 5.0), | |
| threshold=0.1, | |
| inherit_layer_color=True, | |
| ) | |
| assert emitter_idx == 0 | |
| # Burst particles from layer | |
| engine.burst(0, 50) | |
| assert engine.active_count == 50 | |
| # Render frame | |
| rendered = engine.render(bloom=False) | |
| assert rendered.shape == (200, 200, 4) | |
| # The region outside the box [0..100, 0..100] should not have particles born in the lower-right [150..200, 150..200] | |
| lower_right_sum = np.sum(rendered[150:200, 150:200, :3]) | |
| assert lower_right_sum == 0, "No particles should be spawned outside the mask pixels" | |