Particle fountain: Difference between revisions

From Rosetta Code
Content added Content deleted
(Added C++ solution)
m (C++ - removed unnecessary variable)
Line 100: Line 100:
int width_;
int width_;
int height_;
int height_;
int particles_;
std::vector<PointInfo> point_info_;
std::vector<PointInfo> point_info_;
std::vector<SDL_Point> points_;
std::vector<SDL_Point> points_;
Line 113: Line 112:


ParticleFountain::ParticleFountain(int n, int width, int height)
ParticleFountain::ParticleFountain(int n, int width, int height)
: width_(width), height_(height), particles_(n), point_info_(n),
: width_(width), height_(height), point_info_(n), points_(n, {0, 0}),
points_(n, {0, 0}), rng_(std::random_device{}()), dist_(0.0, 1.0) {
rng_(std::random_device{}()), dist_(0.0, 1.0) {
window_.reset(SDL_CreateWindow(
window_.reset(SDL_CreateWindow(
"C++ Particle System!", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
"C++ Particle System!", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,

Revision as of 13:53, 27 September 2021

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 being sprayed up and then falling back down.

The particle fountain should be generally ordered but individually chaotic; the particles should be 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

C++

Library: SDL
Translation of: Raku

<lang cpp>#include <SDL2/SDL.h>

  1. include <algorithm>
  2. include <chrono>
  3. include <cmath>
  4. include <iostream>
  5. include <memory>
  6. include <random>
  7. include <tuple>
  8. include <vector>

auto now() {

   using namespace std::chrono;
   auto time = system_clock::now();
   return duration_cast<milliseconds>(time.time_since_epoch()).count();

}

auto hsv_to_rgb(int h, double s, double v) {

   double hp = h / 60.0;
   double c = s * v;
   double x = c * (1 - std::abs(std::fmod(hp, 2) - 1));
   double m = v - c;
   double r = 0, g = 0, b = 0;
   if (hp <= 1) {
       r = c;
       g = x;
   } else if (hp <= 2) {
       r = x;
       g = c;
   } else if (hp <= 3) {
       g = c;
       b = x;
   } else if (hp <= 4) {
       g = x;
       b = c;
   } else if (hp <= 5) {
       r = x;
       b = c;
   } else {
       r = c;
       b = x;
   }
   r += m;
   g += m;
   b += m;
   return std::make_tuple(Uint8(r * 255), Uint8(g * 255), Uint8(b * 255));

}

class ParticleFountain { public:

   ParticleFountain(int particles, int width, int height);
   void run();

private:

   struct WindowDeleter {
       void operator()(SDL_Window* window) const { SDL_DestroyWindow(window); }
   };
   struct RendererDeleter {
       void operator()(SDL_Renderer* renderer) const {
           SDL_DestroyRenderer(renderer);
       }
   };
   struct PointInfo {
       double x = 0;
       double y = 0;
       double vx = 0;
       double vy = 0;
       double lifetime = 0;
   };
   void update(double df);
   bool handle_event();
   void render();
   double rand() { return dist_(rng_); }
   double reciprocate() const {
       return reciprocate_ ? range_ * std::sin(now() / 1000.0) : 0.0;
   }
   std::unique_ptr<SDL_Window, WindowDeleter> window_;
   std::unique_ptr<SDL_Renderer, RendererDeleter> renderer_;
   int width_;
   int height_;
   std::vector<PointInfo> point_info_;
   std::vector<SDL_Point> points_;
   int num_points_ = 0;
   double saturation_ = 0.4;
   double spread_ = 1.5;
   double range_ = 1.5;
   bool reciprocate_ = false;
   std::mt19937 rng_;
   std::uniform_real_distribution<> dist_;

};

ParticleFountain::ParticleFountain(int n, int width, int height)

   : width_(width), height_(height), point_info_(n), points_(n, {0, 0}),
     rng_(std::random_device{}()), dist_(0.0, 1.0) {
   window_.reset(SDL_CreateWindow(
       "C++ Particle System!", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
       width, height, SDL_WINDOW_RESIZABLE));
   if (window_ == nullptr)
       throw std::runtime_error(SDL_GetError());
   renderer_.reset(
       SDL_CreateRenderer(window_.get(), -1, SDL_RENDERER_ACCELERATED));
   if (renderer_ == nullptr)
       throw std::runtime_error(SDL_GetError());

}

void ParticleFountain::run() {

   for (double df = 0.0001;;) {
       auto start = now();
       if (!handle_event())
           break;
       update(df);
       render();
       df = (now() - start) / 1000.0;
   }

}

void ParticleFountain::update(double df) {

   int pointidx = 0;
   for (PointInfo& point : point_info_) {
       bool willdraw = false;
       if (point.lifetime <= 0.0) {
           if (rand() < df) {
               point.lifetime = 2.5;
               point.x = width_ / 20.0;
               point.y = height_ / 10.0;
               point.vx =
                   (spread_ * rand() - spread_ / 2 + reciprocate()) * 10.0;
               point.vy = (rand() - 2.9) * height_ / 20.5;
               willdraw = true;
           }
       } else {
           if (point.y > height_ / 10.0 && point.vy > 0)
               point.vy *= -0.3;
           point.vy += (height_ / 10.0) * df;
           point.x += point.vx * df;
           point.y += point.vy * df;
           point.lifetime -= df;
           willdraw = true;
       }
       if (willdraw) {
           points_[pointidx].x = std::floor(point.x * 10.0);
           points_[pointidx].y = std::floor(point.y * 10.0);
           ++pointidx;
       }
   }
   num_points_ = pointidx;

}

bool ParticleFountain::handle_event() {

   bool result = true;
   SDL_Event event;
   while (result && SDL_PollEvent(&event)) {
       switch (event.type) {
       case SDL_QUIT:
           result = false;
           break;
       case SDL_WINDOWEVENT:
           if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
               width_ = event.window.data1;
               height_ = event.window.data2;
           }
           break;
       case SDL_KEYDOWN:
           switch (event.key.keysym.scancode) {
           case SDL_SCANCODE_UP:
               saturation_ = std::min(saturation_ + 0.1, 1.0);
               break;
           case SDL_SCANCODE_DOWN:
               saturation_ = std::max(saturation_ - 0.1, 0.0);
               break;
           case SDL_SCANCODE_PAGEUP:
               spread_ = std::min(spread_ + 0.1, 5.0);
               break;
           case SDL_SCANCODE_PAGEDOWN:
               spread_ = std::max(spread_ - 0.1, 0.2);
               break;
           case SDL_SCANCODE_RIGHT:
               range_ = std::min(range_ + 0.1, 2.0);
               break;
           case SDL_SCANCODE_LEFT:
               range_ = std::max(range_ - 0.1, 0.1);
               break;
           case SDL_SCANCODE_SPACE:
               reciprocate_ = !reciprocate_;
               break;
           case SDL_SCANCODE_Q:
               result = false;
               break;
           default:
               break;
           }
           break;
       }
   }
   return result;

}

void ParticleFountain::render() {

   SDL_Renderer* renderer = renderer_.get();
   SDL_SetRenderDrawColor(renderer, 0x0, 0x0, 0x0, 0xff);
   SDL_RenderClear(renderer);
   auto [red, green, blue] = hsv_to_rgb((now() % 5) * 72, saturation_, 1);
   SDL_SetRenderDrawColor(renderer, red, green, blue, 0x7f);
   SDL_RenderDrawPoints(renderer, points_.data(), num_points_);
   SDL_RenderPresent(renderer);

}

int main() {

   std::cout << "Use UP and DOWN arrow keys to modify the saturation of the "
                "particle colors.\n"
                "Use PAGE UP and PAGE DOWN keys to modify the \"spread\" of "
                "the particles.\n"
                "Toggle reciprocation off / on with the SPACE bar.\n"
                "Use LEFT and RIGHT arrow keys to modify angle range for "
                "reciprocation.\n"
                "Press the \"q\" key to quit.\n";
   if (SDL_Init(SDL_INIT_VIDEO) != 0) {
       std::cerr << "ERROR: " << SDL_GetError() << '\n';
       return EXIT_FAILURE;
   }
   try {
       ParticleFountain pf(3000, 800, 800);
       pf.run();
   } catch (const std::exception& ex) {
       std::cerr << "ERROR: " << ex.what() << '\n';
       SDL_Quit();
       return EXIT_FAILURE;
   }
   SDL_Quit();
   return EXIT_SUCCESS;

}</lang>

Julia

Translation of: Raku

<lang julia>using Dates, Colors, SimpleDirectMediaLayer.LibSDL2

mutable struct ParticleFountain

   particlenum::Int
   positions::Vector{Float64}
   velocities::Vector{Float64}
   lifetimes::Vector{Float64}
   points::Vector{SDL_Point}
   numpoints::Int
   saturation::Float64
   spread::Float64
   range::Float64
   reciprocate::Bool
   ParticleFountain(N) = new(N, zeros(2N), zeros(2N), zeros(N), fill(SDL_Point(0, 0), N),
       0, 0.4, 1.5, 1.5, false)

end

function update(pf, w, h, df)

   xidx, yidx, pointidx = 1, 2, 0
   recip() = pf.reciprocate ? pf.range * sin(Dates.value(now()) / 1000) : 0.0
   for idx in 1:pf.particlenum
       willdraw = false
       if pf.lifetimes[idx] <= 0.0
           if rand() < df
               pf.lifetimes[idx]   = 2.5;                       # time to live
               pf.positions[xidx]  = (w / 20)                   # starting position x
               pf.positions[yidx]  = (h / 10)                   # and y
               pf.velocities[xidx] = 10 * (pf.spread * rand() - pf.spread / 2 + recip()) # starting velocity x
               pf.velocities[yidx] = (rand() - 2.9) * h / 20.5; # and y (randomized slightly so points reach different heights)
               willdraw = true
           end
       else
           if pf.positions[yidx] > h / 10 && pf.velocities[yidx] > 0
               pf.velocities[yidx] *= -0.3                  # "bounce"
           end
           pf.velocities[yidx] += df * h / 10                  # adjust velocity
           pf.positions[xidx]  += pf.velocities[xidx] * df     # adjust position x
           pf.positions[yidx]  += pf.velocities[yidx] * df     # and y
           pf.lifetimes[idx]  -= df
           willdraw = true
       end
       if willdraw # gather all of the points that are going to be rendered
           pointidx += 1
           pf.points[pointidx] = SDL_Point(Cint(floor(pf.positions[xidx] * 10)),
               Cint(floor(pf.positions[yidx] * 10)))
       end
       xidx += 2
       yidx = xidx + 1
       pf.numpoints = pointidx
   end
   return pf

end

function fountain(particlenum = 3000, w = 800, h = 800)

   SDL_Init(SDL_INIT_VIDEO)
   window = SDL_CreateWindow("Julia Particle System!", SDL_WINDOWPOS_CENTERED_MASK,
       SDL_WINDOWPOS_CENTERED_MASK, w, h, SDL_WINDOW_RESIZABLE)
   renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED)
   SDL_ClearError()
   df = 0.0001
   pf = ParticleFountain(3000)
   overallstart, close, frames = now(), false, 0
   while !close
       dfstart = now()
       event_ref = Ref{SDL_Event}()
       while Bool(SDL_PollEvent(event_ref))
           event_type = event_ref[].type
           evt = event_ref[]
           if event_type == SDL_QUIT
               close = true
               break
           end
           if event_type == SDL_WINDOWEVENT
               if evt.window.event == 5
                   w = evt.window.data1
                   h = evt.window.data2
               end
           end
           if event_type == SDL_KEYDOWN
               comm = evt.key.keysym.scancode
               if comm == SDL_SCANCODE_UP
                   saturation = min(pf.saturation + 0.1, 1.0)
               elseif comm == SDL_SCANCODE_DOWN
                   saturation = max(pf.saturation - 0.1, 0.0)
               elseif comm == SDL_SCANCODE_PAGEUP
                   spread = min(pf.spread + 1, 50.0)
               elseif comm == SDL_SCANCODE_PAGEDOWN
                   spread = max(pf.spread - 0.1, 0.2)
               elseif comm == SDL_SCANCODE_LEFT
                   range = min(pf.range + 0.1, 12.0)
               elseif comm == SDL_SCANCODE_RIGHT
                   range = max(pf.range - 0.1, 0.1)
               elseif comm == SDL_SCANCODE_SPACE
                   pf.reciprocate = !pf.reciprocate
               elseif comm == SDL_SCANCODE_Q
                   close = true
                   break
               end
           end
       end
       pf = update(pf, w, h, df)
       SDL_SetRenderDrawColor(renderer, 0x0, 0x0, 0x0, 0xff)
       SDL_RenderClear(renderer)
       rgb = parse(UInt32, hex(HSL((Dates.value(now()) % 5) * 72, pf.saturation, 0.5)), base=16)
       red, green, blue = rgb & 0xff, (rgb >> 8) & 0xff, (rgb >>16) & 0xff
       SDL_SetRenderDrawColor(renderer, red, green, blue, 0x7f)
       SDL_RenderDrawPoints(renderer, pf.points, pf.numpoints)
       SDL_RenderPresent(renderer)
       frames += 1
       df = Float64(Dates.value(now()) - Dates.value(dfstart)) / 1000
       elapsed = Float64(Dates.value(now()) - Dates.value(overallstart)) / 1000
       elapsed > 0.5 && print("\r", ' '^20, "\rFPS: ", round(frames / elapsed, digits=1))
   end
   SDL_Quit()

end

println("""

   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.

""")

fountain() </lang>

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>

Phix

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

You can run this online 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 = """
Uparrow increases the saturation of the particle colors,
downarrow decreases saturation until they all become white.
PageUp sprays the particles out at a wider angle/spread,
PageDown makes the jet narrower.
Space toggles reciprocation (wobble) on and off (straight up).
Left arrow decreases the angle range for reciprocation,
right arrow increases the 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/10-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 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)*50   -- 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*8    --             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 [%dx%d])",{title,particlenum,fps,w,h})
        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=400x300")
    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()

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

Wren

Translation of: Julia
Library: DOME
Library: Wren-dynamic

<lang ecmascript>import "dome" for Window, Platform, Process import "graphics" for Canvas, Color import "math" for Math, Point import "random" for Random import "input" for Keyboard import "./dynamic" for Struct

var Start = Platform.time var Rand = Random.new()

var fields = [

   "particleNum",
   "positions",
   "velocities",
   "lifetimes",
   "points",
   "numPoints",
   "saturation",
   "spread",
   "range",
   "reciprocate"

] var ParticleFountain = Struct.create("ParticleFountain", fields)

class ParticleDisplay {

   construct new(particleNum, width, height) {
       Window.resize(width, height)
       Canvas.resize(width, height)
       Window.title = "Wren Particle System!"
       _pn = particleNum
       _w = width
       _h = height
       _df = 1 / 200 // say
       _pf = ParticleFountain.new(
           _pn,                      // particleNum
           List.filled(_pn * 2, 0),  // positions
           List.filled(_pn * 2, 0),  // velocities
           List.filled(_pn, 0),      // lifetimes
           List.filled(_pn, null),   // points
           0,                        // numPoints
           0.4,                      // saturation
           1.5,                      // spread
           1.5,                      // range
           false                     // reciprocate
       )
       for (i in 0..._pn) _pf.points[i] = Point.new(0, 0)
   }
   init() {
       Canvas.cls()
       _frames = 0 
   }
   updatePF() {
       var xidx = 0
       var yidx = 1
       var pointIdx = 0
       var recip = Fn.new { _pf.reciprocate ? _pf.range * Math.sin(Platform.time/1000) : 0 }
       for (idx in 0..._pf.particleNum) {
           var willDraw = false
           if (_pf.lifetimes[idx] <= 0) {
               if (Rand.float() < _df) {
                   _pf.lifetimes[idx]  = 2.5       // time to live
                   _pf.positions[xidx] = _w / 20   // starting position x
                   _pf.positions[yidx] = _h / 10   // and y
                    // starting velocities x and y
                    // randomized slightly so points reach different heights
                   _pf.velocities[xidx] = 10 * (_pf.spread * Rand.float() - _pf.spread / 2 + recip.call())
                   _pf.velocities[yidx] = (Rand.float() - 2.9) * _h / 20.5
                   _willDraw = true
               }
           } else {
               if (_pf.positions[yidx] > _h/10 && _pf.velocities[yidx] > 0) {
                   _pf.velocities[yidx] = _pf.velocities[yidx] * (-0.3)   // bounce
               }
               _pf.velocities[yidx] = _pf.velocities[yidx] + _df * _h / 10             // adjust velocity
               _pf.positions[xidx]  = _pf.positions[xidx] + _pf.velocities[xidx] * _df // adjust position x
               _pf.positions[yidx]  = _pf.positions[yidx] + _pf.velocities[yidx] * _df // and y
               _pf.lifetimes[idx]   = _pf.lifetimes[idx] - _df
               willDraw = true
           }
           if (willDraw) {  // gather all the points that are going to be rendered
               _pf.points[pointIdx] = Point.new((_pf.positions[xidx] * 10).floor,
                                                (_pf.positions[yidx] * 10).floor)
                pointIdx = pointIdx + 1
           }
           xidx = xidx + 2
           yidx = xidx + 1
           _pf.numPoints = pointIdx
       }
   }
   update() {
       if (Keyboard["Up"].justPressed) {
           _pf.saturation = Math.min(_pf.saturation + 0.1, 1)
       } else if (Keyboard["Down"].justPressed) {
           _pf.saturation = Math.max(_pf.saturation - 0.1, 0)
       } else if (Keyboard["PageUp"].justPressed) {
           _pf.spread = Math.min(_pf.spread + 1, 50)
       } else if (Keyboard["PageDown"].justPressed) {
           _pf.spread = Math.max(_pf.spread - 0.1, 0.2)
       } else if (Keyboard["Left"].justPressed) {
           _pf.range = Math.min(_pf.range + 0.1, 12)
       } else if (Keyboard["Right"].justPressed) {
           _pf.range = Math.max(_pf.range - 0.1, 0.1)
       } else if (Keyboard["Space"].justPressed) {
           _pf.reciprocate = !_pf.reciprocate
       } else if (Keyboard["Q"].justPressed) {
           Process.exit()
       }
       updatePF()
   }
   draw(alpha) {
       var c = Color.hsv((Platform.time % 5) * 72, _pf.saturation, 0.5, 0x7f)
       for (i in 0..._pf.numPoints) {
           Canvas.pset(_pf.points[i].x, _pf.points[i].y, c)
       }
       _frames = _frames + 1
       var now = Platform.time
       if (now - Start >= 1) {
           Start = now
           Window.title = "Wren Particle System!    (FPS = %(_frames))"
           _frames = 0
       }
   }

}

System.print("""

   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.

""")

var Game = ParticleDisplay.new(3000, 800, 800)</lang>