Particle fountain

From Rosetta Code
Particle fountain is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

Implement a particle fountain.

Emulate a fountain of water droplets in a gravitational field bring sprayed up and then falling back down.

The particle fountain should be generally ordered but individually chaotic; the particles should being going mostly in the same direction, but should have slightly different vectors.

Your fountain should have at least several hundred particles in motion at any one time, and ideally several thousand.

It is optional to have the individual particle interact with each other.

If at all possible, link to a short video clip of your fountain in action.

Off-site link to a demo video

Phix

Translation of: Raku
Library: Phix/pGUI
Library: Phix/online

You can run my first stab at this online here, though I get a perfectly formed but tiny version and I doubt any of the key handling works. I have asked for ideas elsewhere and will acccept any here.

--
-- demo\rosetta\Particle_fountain.exw
-- ==================================
--
with javascript_semantics
include pGUI.e

Ihandle dlg, canvas
cdCanvas cddbuffer, cdcanvas

constant title = "Particle fountain"
constant help_text = """
Use UP and DOWN arrow keys to modify the saturation of the particle colors.
Use PAGE UP and PAGE DOWN keys to modify the "spread" of the particles.
Toggle reciprocation off / on with the SPACE bar.
Use LEFT and RIGHT arrow keys to modify angle range for reciprocation.
Press the "q" key to quit.
"""

constant particlenum = 3000
-- each particle is {x,y,color,life,dx,dy}
sequence particles = repeat({0,0,0,0,0,0},particlenum)
atom t1 = time()+1
integer fps = 0
bool reciprocate = true
atom range = 1.5,
     spread = 1.5,
     saturation = 0.4,
     start = time(),
     df = 0.0001

function redraw_cb(Ihandle /*ih*/, integer /*posx*/, /*posy*/)
    integer {w, h} = IupGetIntInt(canvas, "DRAWSIZE")
    cdCanvasActivate(cddbuffer)
    cdCanvasClear(cddbuffer)
    for i=1 to length(particles) do
        atom {x,y,color,life} = particles[i]
        if life>0 then
            cdCanvasPixel(cddbuffer, x, h-y, color) 
        end if
    end for
    cdCanvasFlush(cddbuffer)
    return IUP_DEFAULT
end function

function map_cb(Ihandle ih)
    cdcanvas = cdCreateCanvas(CD_IUP, ih)
    cddbuffer = cdCreateCanvas(CD_DBUFFER, cdcanvas)
    cdCanvasSetBackground(cddbuffer, CD_BLACK)
    return IUP_DEFAULT
end function

function hsv_to_rgb(atom h, s, v)
    atom r,g,b
    if s=0 then
        {r,g,b} = {v,v,v}
    else
        integer i = floor(h*6)
        atom f = h*6-i,
             p = v*(1-s),
             q = v*(1-s*f),
             t = v*(1-s*(1-f))
        switch i do
            case 0,
                 6: {r,g,b} = {v, t, p}
            case 1: {r,g,b} = {q, v, p}
            case 2: {r,g,b} = {p, v, t}
            case 3: {r,g,b} = {p, q, v}
            case 4: {r,g,b} = {t, p, v}
            case 5: {r,g,b} = {v, p, q}
        end switch
    end if
    return cdEncodeColor(r*255, g*255, b*255)
end function

function timer_cb(Ihandle /*ih*/)
    integer {w, h} = IupGetIntInt(canvas, "DRAWSIZE")
    fps += 1
    df = time()-start
    start = time()
    for i=1 to particlenum do
        atom {x,y,color,life,dx,dy} = particles[i]
        if life<=0 then
            if rnd()<df then
                life = 2.5          -- time to live
                x = w/2             -- starting position x
                y = h/10            --               and y
                -- randomize velocity so points reach different heights:
                atom r = iff(reciprocate?range*sin(time()):0)
                dx = (spread*rnd()-spread/2+r)*10   -- starting velocity x
                dy = (rnd()-2.9) * h/20.5           --               and y 
                color = hsv_to_rgb(round(remainder(time(),5)/5,100), saturation, 1)
            end if
        else
            if y>h/10 and dy>0 then
                dy *= -0.3  -- "bounce"
            end if
            dy += (h/10)*df -- adjust velocity
            x += dx*df      -- adjust position x
            y += dy*df      --             and y
            life -= df
        end if
        particles[i] = {x,y,color,life,dx,dy}
    end for
    IupRedraw(canvas)
    if time()>t1 then
        IupSetStrAttribute(dlg,"TITLE","%s (%d, %d fps/s)",{title,particlenum,fps})
        t1 = time()+1
        fps = 0
    end if
    return IUP_DEFAULT
end function

function key_cb(Ihandle /*dlg*/, atom c)
    if c=K_ESC or lower(c)='q' then return IUP_CLOSE
    elsif c=K_F1 then   IupMessage(title,help_text)
    elsif c=K_UP then   saturation = min(saturation+0.1,1)
    elsif c=K_DOWN then saturation = max(saturation-0.1,0)
    elsif c=K_PGUP then spread = min(spread+0.1,5)
    elsif c=K_PGDN then spread = max(spread-0.1,0.2)
    elsif c=K_RIGHT then range = min(range+0.1,2)
    elsif c=K_LEFT then range = max(range-0.1,0.1)
    elsif c=K_SP then reciprocate = not reciprocate
    end if
    return IUP_CONTINUE
end function

procedure main()
    IupOpen()
    
    canvas = IupGLCanvas("RASTERSIZE=800x800")
    IupSetCallbacks({canvas}, {"ACTION", Icallback("redraw_cb"),
                               "MAP_CB", Icallback("map_cb")})
    dlg = IupDialog(canvas,`TITLE="%s"`,{title})
    IupSetCallback(dlg, "KEY_CB", Icallback("key_cb"))
    Ihandle timer = IupTimer(Icallback("timer_cb"), 1000/25)
    IupShowXY(dlg,IUP_CENTER,IUP_CENTER)
    IupSetAttribute(canvas, "RASTERSIZE", NULL)
    if platform()!=JS then
        IupMainLoop()
        IupClose()
    end if
end procedure

main()

Perl

<lang perl>#!/usr/bin/perl

use strict; # https://rosettacode.org/wiki/Particle_fountain use warnings; use Tk;

my $size = 900; my @particles; my $maxparticles = 500; my @colors = qw( red green blue yellow cyan magenta orange white );

my $mw = MainWindow->new; my $c = $mw->Canvas( -width => $size, -height => $size, -bg => 'black',

 )->pack;

$mw->Button(-text => 'Exit', -command => sub {$mw->destroy},

 )->pack(-fill => 'x');

step(); MainLoop; -M $0 < 0 and exec $0;

sub step

 {
 $c->delete('all');
 $c->createLine($size / 2 - 10, $size, $size / 2, $size - 10,
   $size / 2 + 10, $size, -fill => 'white' );
 for ( @particles )
   {
   my ($ox, $oy, $vx, $vy, $color) = @$_;
   my $x = $ox + $vx;
   my $y = $oy + $vy;
   $c->createRectangle($ox, $oy, $x, $y, -fill => $color, -outline => $color);
   if( $y < $size )
     {
     $_->[0] = $x;
     $_->[1] = $y;
     $_->[3] += 0.006; # gravity :)
     }
   else { $_ = undef }
   }
 @particles = grep defined, @particles;
 if( @particles < $maxparticles and --$| )
   {
   push @particles, [ $size >> 1, $size - 10,
     (1 - rand 2) / 2.5 , -3 - rand 0.05, $colors[rand @colors] ];
   }
 $mw->after(1 => \&step);
 }</lang>

Raku

Has options to vary the direction at which the fountain sprays, the "spread" angle and the color of the emitted particles. <lang perl6>use NativeCall; use SDL2::Raw;

my int ($w, $h) = 800, 800; my SDL_Window $window; my SDL_Renderer $renderer;

my int $particlenum = 3000;


SDL_Init(VIDEO); $window = SDL_CreateWindow(

   "Raku Particle System!",
   SDL_WINDOWPOS_CENTERED_MASK, SDL_WINDOWPOS_CENTERED_MASK,
   $w, $h,
   RESIZABLE

); $renderer = SDL_CreateRenderer( $window, -1, ACCELERATED );

SDL_ClearError();

my num @positions = 0e0 xx ($particlenum * 2); my num @velocities = 0e0 xx ($particlenum * 2); my num @lifetimes = 0e0 xx $particlenum;

my CArray[int32] $points .= new; my int $numpoints; my Num $saturation = 4e-1; my Num $spread = 15e-1; my &reciprocate = sub { 0 } my $range = 1.5;

sub update (num \df) {

   my int $xidx = 0;
   my int $yidx = 1;
   my int $pointidx = 0;
   loop (my int $idx = 0; $idx < $particlenum; $idx = $idx + 1) {
       my int $willdraw = 0;
       if (@lifetimes[$idx] <= 0e0) {
           if (rand < df) {
               @lifetimes[$idx]   = 25e-1;                       # time to live
               @positions[$xidx]  = ($w / 20e0).Num;             # starting position x
               @positions[$yidx]  = ($h / 10).Num;               # and y
               @velocities[$xidx] = ($spread * rand - $spread/2 + reciprocate()) * 10; # starting velocity x
               @velocities[$yidx] = (rand - 2.9e0) * $h / 20.5;    # and y (randomized slightly so points reach different heights)
               $willdraw = 1;
           }
       } else {
           if @positions[$yidx] > $h / 10 && @velocities[$yidx] > 0 {
               @velocities[$yidx] = @velocities[$yidx] * -0.3e0; # "bounce"
           }
           @velocities[$yidx] = @velocities[$yidx] + $h/10.Num * df;         # adjust velocity
           @positions[$xidx]  = @positions[$xidx] + @velocities[$xidx] * df; # adjust position x
           @positions[$yidx]  = @positions[$yidx] + @velocities[$yidx] * df; # and y
           @lifetimes[$idx]   = @lifetimes[$idx] - df;
           $willdraw = 1;
       }
       if ($willdraw) {
           $points[$pointidx++] = (@positions[$xidx] * 10).floor; # gather all of the points that
           $points[$pointidx++] = (@positions[$yidx] * 10).floor; # are still going to be rendered
       }
       $xidx = $xidx + 2;
       $yidx = $xidx + 1;
   }
   $numpoints = ($pointidx - 1) div 2;

}

sub render {

   SDL_SetRenderDrawColor($renderer, 0x0, 0x0, 0x0, 0xff);
   SDL_RenderClear($renderer);
   SDL_SetRenderDrawColor($renderer, |hsv2rgb(((now % 5) / 5).round(.01), $saturation, 1), 0x7f);
   SDL_RenderDrawPoints($renderer, $points, $numpoints);
   SDL_RenderPresent($renderer);

}

enum KEY_CODES (

   K_UP     => 82,
   K_DOWN   => 81,
   K_LEFT   => 80,
   K_RIGHT  => 79,
   K_SPACE  => 44,
   K_PGUP   => 75,
   K_PGDN   => 78,
   K_Q      => 20,

);

say q:to/DOCS/; Use UP and DOWN arrow keys to modify the saturation of the particle colors. Use PAGE UP and PAGE DOWN keys to modify the "spread" of the particles. Toggle reciprocation off / on with the SPACE bar. Use LEFT and RIGHT arrow keys to modify angle range for reciprocation. Press the "q" key to quit. DOCS

my $event = SDL_Event.new;

my num $df = 0.0001e0;

main: loop {

   my $start = now;
   while SDL_PollEvent($event) {
       my $casted_event = SDL_CastEvent($event);
       given $casted_event {
           when *.type == QUIT {
               last main;
           }
           when *.type == WINDOWEVENT {
               if .event == RESIZED {
                   $w = .data1;
                   $h = .data2;
               }
           }
           when *.type == KEYDOWN {
               if KEY_CODES(.scancode) -> $comm {
                   given $comm {
                       when 'K_UP'    { $saturation = (($saturation + .1) min 1e0) }
                       when 'K_DOWN'  { $saturation = (($saturation - .1) max 0e0) }
                       when 'K_PGUP'  { $spread = (($spread + .1) min 5e0) }
                       when 'K_PGDN'  { $spread = (($spread - .1) max 2e-1) }
                       when 'K_RIGHT' { $range = (($range + .1) min 2e0) }
                       when 'K_LEFT'  { $range = (($range - .1) max 1e-1) }
                       when 'K_SPACE' { &reciprocate = reciprocate() == 0 ?? sub { $range * sin(now) } !! sub { 0 } }
                       when 'K_Q'     { last main }
                   }
               }
           }
       }
   }
   update($df);
   render();
   $df = (now - $start).Num;
   print fps();

}

say ;

sub fps {

   state $fps-frames = 0;
   state $fps-now    = now;
   state $fps        = ;
   $fps-frames++;
   if now - $fps-now >= 1 {
       $fps = [~] "\r", ' ' x 20, "\r",
           sprintf "FPS: %5.1f  ", ($fps-frames / (now - $fps-now));
       $fps-frames = 0;
       $fps-now = now;
   }
   $fps

}

sub hsv2rgb ( $h, $s, $v ){

   state %cache;
   %cache{"$h|$s|$v"} //= do {
       my $c = $v * $s;
       my $x = $c * (1 - abs( (($h*6) % 2) - 1 ) );
       my $m = $v - $c;
       [(do given $h {
           when   0..^1/6 { $c, $x, 0 }
           when 1/6..^1/3 { $x, $c, 0 }
           when 1/3..^1/2 { 0, $c, $x }
           when 1/2..^2/3 { 0, $x, $c }
           when 2/3..^5/6 { $x, 0, $c }
           when 5/6..1    { $c, 0, $x }
       } ).map: ((*+$m) * 255).Int]
   }

}</lang>

Link to off-site .mp4 video