BrianHicks / elm-particle / Particle

Particle

This module lets you particles! You'll need to define:

  1. how it acts, using things like withLocation and withDirection.
  2. what it looks like, which you'll provide in the type parameter to Particle.

For example, maybe you want to show some confetti when a student finishes a quiz. Hooray! Time to celebrate! 🎉 🎊 We'll model each little piece as having both a color and a shape, like this:

type alias Confetti =
    { color : Color -- Red, Green, Blue
    , shape : Shape -- Square, Star, Streamer
    }

Then a Particle of confetti—just one of those little pieces—would be Particle Confetti. Boring, but in the best way possible! Now, we could construct our confetti by hand, like this:

{ color = Red, shape = Square }

… but that's boring! Not only do we have to do every piece we want by hand, but since all functions in Elm are pure we will never get any variation! Boo! Instead, we'll generate Confetti randomly using elm/random. If you haven't used this package before, check out The Elm Guide's explanation, or Chandrika Achar's appearance on Elm Town. We'll use Random.map2 and Random.uniform to generate particles of a random color and shape:

The code ends up looking like this:

confetti : Random.Generator Confetti
confetti =
    Random.map2 Confetti
        (Random.uniform Red [ Green, Blue ])
        (Random.uniform Square [ Star, Streamer ])

So that's the data for rendering your particles, but how do you get them to behave how you like? When using Particle, you'll create a particle with init, and then use functions like withLocation and withDirection to define that. Read on for what they do!

You should also read the documentation on Particle.System for a managed way to render these—you don't have to worry about animation yourself!

One last thing before we get into the documentation in earnest: this page only scratches the surface of what you can do with particle generators. There are a few fully-worked-out and documented examples in the examples folder of the source on GitHub. Go check those out!

Constructing Particles


type Particle a

A single particle, doing... something? Who knows! You get to define that! See the top of the module docs for how this all fits together.

init : Random.Generator a -> Random.Generator (Particle a)

Start making a particle, given a generator for the data you want to use to render your particle.

init confetti

withLifetime : Random.Generator Basics.Float -> Random.Generator (Particle a) -> Random.Generator (Particle a)

You don't normally want particles to live forever. It means calculating a lot of deltas that you don't care about, and which the person using your software will never see. So, let's give them some lifetimes! The unit here is seconds.

init confetti
    |> withLifetime (Random.constant 1)

We use another Random.Generator here, since it looks nicer for particles which have been introduced in a burst all at once to fade out progressively instead of all at once. You can use Random.Float.normal from elm-community/random-extra to do this. For example: normal 1 0.1. This generates a normal distribution with a mean of the first number and a standard deviation of the second, so it will not be precisely 0.9 to 1.1 seconds, but normal tends to produce pretty good results!

Note: In the future, it may be possible for Particle.System.System to automatically remove particles which have gone off screen. For now, lifetimes are the best system we have for this!

withDelay : Random.Generator Basics.Float -> Random.Generator (Particle a) -> Random.Generator (Particle a)

You might want your particles to appear over a little time, instead of all at once. This is how you get that effect without doing a lot of Elm Architecture plumbing. Like withLifetime, this uses seconds instead of milliseconds.

This code will produce particles that live 1 second, but don't show up until between 0 and 1 seconds after the initial event:

init confetti
    |> withLifetime (Random.constant 1)
    |> withDelay (Random.float 0 1)

It's OK if your generator produces negative values; we'll just call abs on them before rendering. This makes it way easier to use Random.Float.normal to get a bunch of particles to show up at once but trail off over time (e.g. normal 0 0.25 would be centered around 0 with a standard deviation of 0.25 seconds.)

withLocation : Random.Generator { x : Basics.Float, y : Basics.Float } -> Random.Generator (Particle a) -> Random.Generator (Particle a)

Where should this particle start it's life? { x = 0, y = 0} is at the top left of the image. So we can render in the center like this:

init confetti
    |> withLocation (Random.constant { x = width / 2, y = height / 2 })

Or at a random location on screen like this:

init confetti
    |> withLocation
        (Random.map2 (\x y -> { x = x, y = y })
            (Random.map (modBy width << abs) Random.float)
            (Random.map (modBy height << abs) Random.float)
        )

withDirection : Random.Generator Basics.Float -> Random.Generator (Particle a) -> Random.Generator (Particle a)

In what direction is this particle traveling, to start?

withDirection uses Elm Standard Units™ (radians.) 0 is straight up, and rotation goes clockwise. You can, of course, substitute degrees 45 or turns 0.125 if that's easier for you to reason about—I prefer degrees, myself!

init confetti
    |> withDirection (Random.Float.normal (degrees 45) (degrees 10))

withSpeed : Random.Generator Basics.Float -> Random.Generator (Particle a) -> Random.Generator (Particle a)

How fast is this particle traveling traveling, to start?

In this case, speed is a rough measurement—it's close to but not exactly pixels per second, so you'll have to experiment to make it look good for your use case.

init confetti
    |> withSpeed (Random.Float.normal 300 100)

withGravity : Basics.Float -> Random.Generator (Particle a) -> Random.Generator (Particle a)

Is this particle affected by gravity?

The unit here ends up being pixels per second per second. If you want something earthlike, you'll probably want 9.8 * dots/meter. Buuut that's also super fast, and you probably want something slightly slower and more cartoony. 980 works well!

init confetti
    |> withGravity 980

This takes a constant, while its siblings take generators. Why is this? Well, unlike position, heading, or lifetime, you probably do want all your particles to have the same gravity! (Or at least, you want a few groupings of gravity, not every particle being affected differently.)

Note: under the covers, this is really modeling acceleration over time, so it's not only gravity. But, I can't think of anything offhand I need this for other than gravity! So if you have a concrete use case for going sideways or up, open an issue and let me know!

withDrag : (a -> { density : Basics.Float, area : Basics.Float, coefficient : Basics.Float }) -> Random.Generator (Particle a) -> Random.Generator (Particle a)

How is this particle affected by the surrounding environment? Is there air? Water? Setting the right resistance will help your particles look more realistic! You'll have to tweak these numbers to get something you like; here's what they mean:

This function is a bit different from others because it's really convenient to be able to generate whatever kind of particle and set drag separately. You could structure your code so that this would not be a concern, but it gets annoying to have to care about it it in multiple places. So, if we have these shapes in our Particle Shape:

type Shape
    = Circle Float
    | Square Float

We'd call withDrag like this:

init shapeGenerator
    |> withDrag
        (\shape ->
            { density = 0.001275
            , coefficient =
                case shape of
                    Circle _ ->
                        0.47

                    Square _ ->
                        1.05
            , area =
                case shape of
                    Circle radius ->
                        radius * 2

                    Square side ->
                        side
            }
        )

coefficients

Rendering Particles

view : (Particle a -> Svg msg) -> Particle a -> Svg msg

Hey! You should probably be looking at the docs for Particle.System.view, which has the same signature but works with all your particles at once.

Render the particle as SVG. I'll give you the particle, and you use functions like data and lifetimePercent to get the data you need for rendering. It might look like this:

view <|
    \particle ->
        case Particle.data particle of
            Square { color } ->
                Svg.rect
                    [ Svg.Attributes.width "10"
                    , Svg.Attributes.height "10"
                    , Svg.Attributes.fill (Color.toHex color)
                    ]
                    []

            _ ->
                -- other shapes here

You don't need to set the location of the particle, as it'll be done for you by wrapping whatever you pass in a <g> element.

viewHtml : (Particle a -> Html msg) -> Particle a -> Html msg

Do the same thing as view but render HTML instead of SVG.

data : Particle a -> a

Get the data you passed in out of a particle, for use in view functions.

lifetimePercent : Particle a -> Basics.Float

Get the remaining lifetime of a particle, for use in view functions. This returns a number between 0 and 1, which is useful for setting opacity to smoothly fade a particle out instead of having it just disappear.

lifetime : Particle a -> Basics.Float

Get how long a particle has been alive, in seconds. This is mostly useful for deciding whether or not to display a particle (if it's been delayed, this value will be a countdown to when it should show up.) In most cases, lifetimePercent will be much more useful!

direction : Particle a -> Basics.Float

Get the direction the particle is currently facing. This is useful for particles whose shape implies a direction, like arrows or boxes.

Heads up! The most common use of this function is probably to set rotation on the particle. That's fine, but the rotate transformation can only use degrees, and this function returns radians. Use directionDegrees instead so you can avoid doing the math yourself.

directionDegrees : Particle a -> Basics.Float

Like direction but returns the angle in degrees instead of radians to make SVG transformations easier.

speed : Particle a -> Basics.Float

Get the speed the particle is currently traveling. This is useful for doing things like stretching or squashing the shape in response to changes in motion.

leftPixels : Particle a -> Basics.Float

Get the position from the left side of the screen in pixels. You only need this when using Particle.System.viewCustom.

topPixels : Particle a -> Basics.Float

Get the position from the top side of the screen in pixels. You only need this when using Particle.System.viewCustom.

Simulation

update : Basics.Float -> Particle a -> Maybe (Particle a)

Hey! You probably shouldn't use this! Instead, manage all your particles at once with the functions in Particle.System!

That said, this updates a single particle, given a delta in milliseconds.