This module lets you particles! You'll need to define:
withLocation
and
withDirection
.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:
Random.uniform
takes a bunch of items, and chooses between them evenly.Random.map2
takes the random stuff you generate, and gives it as two
arguments to a function.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!
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:
density is the density of whatever fluid the particles are in. The higher this is, the more particles will be slowed down. Think about trying to run on land versus in the water—you're slowed down much more by the water than the air, and you experience more resistance the faster you try to move. Air will be around 0.001275 (g/cm³). Water will be around 1.
area is the area of the front surface. For a square, that'd be the side facing into the flow. The bigger this is, the more drag happens. When setting this, remember that this is a 2-dimensional simulation, so you mostly just provide a single dimension's length! In a real simulation, we'd calculate this on every frame to account for rotation. But we can get acceptable results without that, so it's fine to just give a rough number here!
coefficient is how easily air/water/whatever flows over the surface facing into the flow. A higher number means that you will face more resistance. [Wikipedia][coefficients] has a nice chart of sample coefficients for various surface shapes; choosing one of those will probably get you most of the way there.
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
}
)
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
.
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.