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"