agrfun 1.0.0

Download zip

agrfun

Ready-made graphics for the AGR graphics window. Each function draws something into the FXGL game that AGR is already running, so there is no game to launch and no window to open -- call one and watch.

Requirements

These functions reach for a running FXGL game, so they only work inside AGR (or inside a test that has launched a game of its own). A script run from the command line has no game behind it.

AGR includes fxgl for you at the start of every workspace, so the only include you need is this package.

Usage

include agrfun.agrfun;

agrfun.comet();

How the package is laid out

Each graphic is a class in a file of its own, and agrfun is a short way in:

File Holds
agrfun.aus agrfun, the static class you call
Bomber.aus the Bomber class
Comet.aus the Comet class
Filigree.aus the Filigree class
Fireworks.aus the Fireworks class
Fractals.aus the Fractals class
ParticleBurst.aus the ParticleBurst class
Schultz.aus the Schultz class
images/ pictures the graphics use

agrfun.comet() is only three lines: it builds a Comet, runs it, and hands back nothing.

None of these functions returns anything. They are shortcuts meant to be typed at a console, and a returned entity or duration is just noise printed back at you. When you want the graphic itself -- to find it, move it, time something against it -- build the class:

agrfun.comet();          // draws a comet, returns null

ct = new Comet();
e = ct.run();            // the same comet, and its Entity

Use the class directly when you want to read how a graphic works, or when you want to change its settings. Every number that shapes a graphic is a member at the top of its class, with a comment saying what it does.

Functions

bomber()

Flies a bomber across the FXGL window and drops a stick of bombs.

The plane comes in off the left edge, flies level at a random height in the top half of the window, and leaves off the right. The whole crossing takes two to three seconds. Somewhere in the left half it lets go of three to five bombs, one after another.

A bomb leaves with half the plane's forward speed and only gravity acts on it, which is what makes it arc away ahead of where it was dropped instead of falling straight. The plane holds its own speed, so it draws about a plane's length ahead of the stick by the time the bombs land. Each one bursts into a shower of sparks when it reaches the bottom of the window. Height, speed, where the stick starts, how many bombs are in it and how far they carry are drawn fresh on every call.

The plane, the bombs, and the blasts all clear themselves out of the game world when they are done.

Returns null. Bomber.run() hands back the plane Entity.

agrfun.bomber();

The plane is tagged "agrfunBomber", the bombs "agrfunBomb", and the blasts "agrfunBlast". The bombs and blasts arrive on timers, so none of them is in the world yet when the call returns.

comet()

Sends a small comet arcing across the FXGL window.

The comet is a bright round head with a tail of shrinking, fading circles behind it, all under a glow. It enters at a random point on one edge of the window and heads for a random point on a different edge, so it always crosses the full width or height rather than clipping a corner. It does not travel in a straight line: a steady sideways pull bends the flight into an arc that bows left or right of that line.

Entry edge, exit edge, both points, speed, head size, and the depth and direction of the arc are drawn fresh on every call, so no two comets follow the same path.

The comet removes itself from the game world once it is off screen, so calling it over and over does not pile up entities.

Returns null. Comet.run() hands back the comet Entity.

// One comet.
agrfun.comet();

// A shower of them.
i = 0;
while (i < 12) {
    agrfun.comet();
    i = i + 1;
}

Since the function hands nothing back, the way to find the ones in flight is the type tag:

include fxgl.GameWorld;

world = fxgl.getGameWorld();
comets = world.getEntitiesByType(["agrfunComet"]);

filigree()

Grows a filigree pattern across the FXGL window.

A seed lands a little way in from a random edge and sends two or three stems out from it. Each stem is drawn one short segment at a time and turns a little more sharply with every segment, so it sets off on a wide sweep and winds into a scroll at its end. Partway along, a stem throws off a branch to either side, and those branches do the same, three generations deep. Leaves open out along every tendril as it grows, turn and turn about on either side, and every tendril that reaches its full length finishes with a small round flourish.

The drawing takes ten seconds. The pattern then stands finished for three, fades out over two, and clears itself out of the game world, so calling it over and over does not pile up entities. Seed point, edge, stem count, colour, and the length and lean of every tendril are drawn fresh on every call.

A leaf is sized from the line width of the tendril that put it out, so the stems carry large leaves and the fine branches small ones.

Returns null. Filigree.run() hands back the pattern Entity -- the whole pattern is one entity carrying one group of lines, which fills up as the drawing goes on.

agrfun.filigree();

The entity is tagged "agrfunFiligree". Note that only the stems are drawn when the call returns; everything else arrives over the next ten seconds.

Use the class directly to change the timing:

fg = new Filigree();
fg.run();
c.log(fg.total());     // 15.0 -- drawing, holding, and fading

fireworks()

Sets off a display of fireworks across the FXGL window.

Six shells go off at random points in the upper part of the window, spread over about two seconds. Each one throws out a ring of sixty coloured sparks that arc away from the burst, fall under gravity, and fade to nothing the way a real firework does. Positions, colours, burst sizes, timings and spark lifetimes are drawn fresh on every call.

Each burst clears itself out of the game world once its last spark has burned out, so nothing is left behind.

Returns null. Fireworks.run() hands back the number of bursts set off.

agrfun.fireworks();

Bursts are tagged with the entity type "agrfunFirework". Note that they go off on a timer, so only the first is in the world when the call returns.

fractals()

Flies the FXGL window into a fractal curve.

The window starts with the whole curve in view and spends ten seconds closing in on one point of it, ending four hundred times nearer. Because the curve is the same shape at every scale, there is always more of it to find: detail too fine to draw at the start is what fills the window at the end. It then holds still for a second and a half, fades out over two, and clears itself out of the game world.

The curve is a steep-peaked Koch snowflake or a Sierpinski gasket, picked at random, and so are the colour and the point it flies into. Both are dense enough to fill the window at any depth.

Returns null. Fractals.run() hands back the curve Entity.

agrfun.fractals();

The entity is tagged "agrfunFractal". Ask the class which curve it drew:

fr = new Fractals();
fr.run();
c.log(fr.name());      // "snowflake" or "gasket"
c.log(fr.total());     // 13.5 -- flying in, holding, and fading

particleBurst()

Blows a burst of dots apart in the middle of the FXGL window.

The dots leave together in every direction, spread out over their whole burn, and fade as they go. How far they reach is measured against the window, so a burst fills the same share of it whatever size the window is. The whole thing is over in about five seconds and clears itself out of the game world.

How many dots is the argument, since it is the one thing a caller usually wants to set:

agrfun.particleBurst();          // two hundred dots
agrfun.particleBurst(1000);      // a thousand

Returns null. ParticleBurst.run() hands back the burst Entity, which is tagged "agrfunBurst".

A thousand dots is not free. Measured headless in a 1473 x 917 window, against an empty game's 61 frames a second:

Dots Frame rate
200 52
1000 32

Most of that is the additive blending FXGL's explosion emitter uses, which is what makes overlapping dots glow -- turning it off with setBlendMode("SRC_OVER") gets a thousand dots back to 45 frames a second and costs the burst its brightest quality. The frame rate is worth watching rather than worrying about: as long as it stays above about 13 a second the graphic only looks coarser, not slower (see Every time in this package is game time).

schultz()

Has Sergeant Schultz lean in at the edge of the window while "NO-THING" fades up in the middle.

He slides in at a random spot on a random edge until he is flush against it, waits a couple of seconds, then leaves the way he came. His feet always face the edge he came in from, so arriving from above means arriving upside down. The words fade up as he slides in and fade away as he slides out, so the two move together. Both clear themselves out of the game world at the end.

Returns null. Schultz.run() hands back how many seconds the whole appearance takes, so you can line something else up behind it.

agrfun.schultz();

The picture is tagged "agrfunSchultz" and the words "agrfunSchultzText".

How the bomber is built

Three kinds of entity, all built from stock FXGL parts:

The arithmetic behind the drop

The interesting part is that the drop has to satisfy two rules at once: the bombs must leave the plane in the left half of the window, and they must come down inside it. Nothing acts on a bomb sideways, so how far it carries ahead is bomb speed x fall time -- and a fast plane can easily throw its bombs off the right edge.

planStick settles it by working backwards from the landing:

  1. Pick where the stick starts, in the left half, and space the bombs a fixed distance apart. Because the room the rest of the stick needs is taken off the top of the range, the last bomb is still away inside the left half.
  2. Take the room the bombs fall through as a share of what is left ahead of that last bomb. Whatever the plane's speed, the whole stick then lands inside the window.
  3. That room is measured against the plane, so the fall lasts as long as the plane takes to cross it: fall time = room / plane speed. The bombs are slower, so they cover only their share of that room -- which is exactly the gap that opens up between the plane and its stick.
  4. A drop of depth pixels in that time needs a pull of 2 x depth / time^2, which is the gravity every bomb in the stick gets.

Why the bombs do not keep the whole of the plane's speed

Giving a bomb the plane's exact forward speed is the tidier physics, and it is what this did at first. It looks wrong. Both are then moving forward at the same rate, so the bomb sits directly under the bomb bay for the whole of its fall, and what the eye reads is not a bomb falling away but the plane hanging back to stay with it.

That was easy to miss while the crossing took four seconds and impossible to miss once it took eight. bombSpeedShare is the fix: at half the plane's speed the gap opens at speed / 2 and the plane is the better part of its own length ahead by the time the stick lands. A real bomb meets the air and falls behind the aircraft in much the same way.

Because they share a gravity, a drop height and a speed, all the bombs fall for the same length of time and land the same distance apart as they were dropped -- a neat row of craters rather than a heap.

One useful thing falls out of working it this way. Every time in the graphic comes from minFlightSeconds / maxFlightSeconds and every distance is fixed independently of them, so those two dials set the pace of the whole thing. Treble the crossing time and the bombs are let go in the same places, land in the same places, and simply take three times as long to get there -- the gravity comes out at a ninth, which is exactly what keeps the arc the same shape. There is no second dial to keep in step.

The bombs are staggered with Timer.runOnceAfter, twice each: once to be let go, and once, a fall later, to land. As with the fireworks, a timer callback takes no arguments, so run() works the whole stick out up front and parks it in a list; dropNext() and blastNext() then take the next entry along.

How the comet is built

The comet is an ordinary FXGL entity assembled from stock parts, which is worth knowing if you want to build something similar of your own:

Two bits of the arithmetic are worth reading in the source:

How the filigree is built

Unlike the other graphics, the filigree is one entity that changes as you watch it rather than a set of entities that move. It carries a single fx.Group under an fx.Glow, and the drawing happens by adding children to that group over time:

The leaves

A tendril puts a leaf out every leafEvery steps, alternating sides and leaning leafLean radians off whatever heading it had at the time. Each one is an fx.SVGPath: two quadratic curves meeting at a point at each end, which is the cheapest way to draw a leaf shape.

Two things make them look grown rather than stamped on:

The leaves go in a group of their own, added to the view before the tendrils, so a leaf never covers the line it grew from.

The growing tip

The pattern is grown, not drawn from a plan. The class keeps a list of tips, one per tendril still growing. A tip is a position, a heading, a turn per step, an age, and the Polyline it is drawing into. One growth step does the same four things to every live tip:

  1. Step one short segment along the heading and append that point.
  2. Turn the heading by the tip's current turn.
  3. Multiply that turn by a fixed amount, so the next bend is sharper.
  4. Age it by one, and drop it if it has run out of life or left the window.

Step 3 is the whole trick. A constant turn per step draws a circle; a turn that grows draws a spiral that starts wide and winds in -- which is what a filigree scroll is.

The scroll arithmetic

Two dials shape a tendril, and both are read from the tendril's own length rather than from the window, which is what makes a short branch draw the same scroll as a long stem, only smaller.

Because the turn is multiplied by the same amount every step, the turns make a geometric series. If a tendril gets n steps and the last turn is to be R times the first, then the multiplier is R^(1/n), and the series sums to stemTurn when the first turn is

stemTurn * (multiplier - 1) / (R - 1)

Everything else about the shape falls out of those two numbers.

Branching, and running out of time

A tip throws a branch at two fixed points in its life, one to each side, and the branches do the same until maxDepth generations down. Each generation is shorter and thinner than the one above it, so the pattern is coarse at the base and fine at the tips.

The drawing is given a fixed budget of growth steps -- growSeconds times stepsPerSecond -- and no tendril may outlive it. A branch is given the shorter of its natural length and the steps left in the budget, and one with fewer than a handful left is not started at all. That is what makes the pattern finish cleanly at ten seconds instead of trailing off: the last branches to be thrown are the short ones that fit.

Drawing to the clock, not to the timer

Timer.runAtInterval can only fire on a frame, so asking for a step every 0.04s really gets one every three frames -- and the drawing runs a third longer than it should. Instead the job fires every frame and works out how many steps are due from the game clock:

steps due = totalSteps * (now - startedAt) / growSeconds

and takes however many are outstanding. The drawing then takes growSeconds whatever rate the game happens to run at, which matters because the fade is scheduled against the same clock and would otherwise start while the pattern was still growing.

Staying in the window

A tendril is dropped the moment it steps outside the window, so a scroll that wanders off is cut rather than drawn where nobody can see it. Two things keep that from eating the pattern:

How the fractal fly in is built

A fractal has detail at every scale, so nothing can be drawn once and zoomed into: at some point the picture runs out of detail and the lines just get fatter. The fly in gets round that by drawing the curve again every time the window has closed in far enough to show more of it.

One rule, two curves

Both curves come from the same idea. Take a straight line, replace it with a set of shorter lines, and do the same to each of those, forever. The rule is written as the points the replacement passes through, in a frame where the line runs from (0, 0) to (1, 0):

Curve Rule What it draws
snowflake a steep peak in the middle a Koch snowflake
gasket two points at a third of the way up a Sierpinski gasket

The snowflake's peak is much steeper than the 60 degrees of the textbook Koch curve, which leaves each of its four shorter lines 0.414 of the line it replaced rather than a third. That is what fills the window. The gentler the peak, the more of the curve's length goes into a thin crinkled ribbon with bare background either side of it; steepening it packs the same length into a dense lace of nested stars. It is the one dial that decides whether a fly in has anything to look at.

The gasket needs one extra thing. Its outer two shorter lines carry the rule on mirrored, which is what folds a straight line into a triangle instead of a staircase. So each rule also carries a flip flag per shorter line, and a line's own flip is its parent's flip turned over whenever the flag is set.

That is the whole of the curve. Everything else is deciding which parts of it to draw.

Drawing only what shows

step deals with one straight line at a time and does one of three things with it:

Because the rule keeps every shape, growing the curve and then scaling it to the window gives the same picture as scaling first and growing after. The fly in scales first, so every length step measures is already in pixels and can be held against detail straight off.

Together these keep a drawing to the size of the window rather than the size of the curve. However deep the fly in goes, a drawing is a few thousand lines.

Two things that have to be spread out

The whole drawing goes into an fx.SVGPath, whose shape is set from a string in a single call. That matters: an fx.Polyline takes its points one at a time across the Java bridge, and measured on 3000 points that is 390ms against 8ms for the same shape as a path string.

Even so, a redraw takes about a tenth of a second -- six frames' worth -- so doing one inside a single frame stutters the fly in every time. So a redraw is spread over the frames until the next one is due: startBuild lines the work up, advanceBuild gets through stepsPerFrame lines of it per frame, and finishBuild hands the finished string to both strokes at once. What is on screen is left alone until then.

Between redraws

The entity carries a scale, and the drawing is made with the point being flown into at the local origin, so scaling about that origin closes the window in without moving it off centre. Between redraws that scale carries the whole fly in:

stretch = how close the window is now / how close the drawing was made for

Two things follow. The stretch is never below 1, because a drawing is always made for where the view already is -- stretching below 1 would show more of the drawing than it was made for and leave a bare edge where the curve was cut off. And the line has to be thinned by the same amount it is stretched, or a 400 times closer view would draw it 400 times fatter.

The closeness itself is read off the game clock, like the filigree's growth, and it closes in by the same fraction every second rather than the same distance -- which is what makes a fly in look even rather than grinding to a halt.

No glow

The comet and the filigree both sit under an fx.Glow. The fractal does not, and the reason is worth knowing before you reach for one. A glow is an effect, so JavaFX draws the node to an offscreen image and filters it every frame. Measured on a curve this size that costs two thirds of the frame rate -- 22 frames a second against 58 -- while the size of the drawing barely matters at all. The same look comes free from drawing the curve twice: a wide faint stroke with a narrow bright one on top.

How the particle burst is built

One entity carrying a ParticleComponent built from FXGL's ready-made explosion emitter, the same part the fireworks and the bomber's blasts use. Three settings turn it into a burst:

The number FXGL calls a radius

ParticleEmitters.newExplosionEmitter takes one number. FXGL calls it the explosion radius, and it is neither a radius nor a speed: it is a rate. A dot covers about 0.82 pixels per unit of it per second of burn, so how far the dots get depends on how long they are given.

That took two measurements to pin down. Sweeping the number at a fixed burn gives a clean straight line, which reads like a distance:

throw  10  ->  radius  11        throw  50  ->  radius  55
throw  25  ->  radius  27        throw 120  ->  radius 131

But doubling the burn on the same number doubled the radius too -- 264 pixels became 526. The dots had not slowed down; they had simply been given twice as long to travel.

So the class keeps its dial as a real distance -- a share of the shorter side of the window -- and divides by both the rate and the burn on the way in. Two things follow. A burst fills the same part of a small window as a large one; and the burn is the pace dial, because halving the speed means doubling the burn and halving the throw, exactly as the fireworks do.

Worth knowing if you measure particles yourself: FXGL places a particle view with setLayoutX/Y in window coordinates, not relative to the burst. Forgetting to take the middle of the window off first gives every dot the same distance from the origin, and a burst that appears not to move at all.

How the fireworks are built

Each burst is an entity carrying a ParticleComponent built from FXGL's ready-made explosion emitter. Three settings turn that into a firework:

The shells are staggered with Timer.runOnceAfter. Because a timer callback takes no arguments, run() works out every shell up front and parks it in a list; each timer then calls fireNext(), which sets off the next one along.

Two dials decide how a burst moves, and they work together. Sparks leave at minSparkSpeed to maxSparkSpeed pixels per second, slow down as they go, and burn for minLife to maxLife seconds. A spark ends up roughly speed x life from the middle of the burst, so the size comes out of both.

That gives a simple rule for changing the pace without changing the picture: halve the speed and double the burn time, and you get the same size of burst at half the speed. Drop gravity to a quarter at the same time and the sparks still fall along the same curve, because the distance gravity pulls them grows with the square of the time.

A spark that leaves the window before its time is up disappears there instead, so very long burn times mostly show up as sparks drifting off the edges.

Note that FXGL keeps particle views on a second entity of its own, reachable through ParticleComponent.getParent() -- not on the burst entity. Both are cleaned up when the burst expires.

How Schultz is built

Two entities and two animations:

Two things to know if you build on this:

Tuning

Every number that shapes a graphic is a member at the top of its class -- head size, speed, arc depth, trail length, burst count, spark lifetime, gravity, colours. Change them there and rerun.

Every time in this package is game time

A speed of 70 means 70 pixels per second of game time, and game time is not the wall clock. FXGL advances it by tpf once a frame and clamps that into a band, measured here as a floor near 1/60 of a second and a ceiling of 1/13. So:

Host frame rate Game time per real second What you see
240 fps 4.0 s everything runs four times too fast
60 fps 1.0 s the speed the dial says
13 fps 1.0 s correct, but visibly juddering
6 fps 0.5 s everything runs at half speed

Cap the host at 60 frames a second and every dial here means what it says. Uncapped, an idle window can render at two or three hundred frames a second and run the graphics at three or four times their intended speed -- until something loads a frame, at which point they drop to their real pace, which reads as the graphic suddenly slowing down.

Do not tune the dials against an uncapped host: you will be correcting for a frame rate rather than for the graphic, and the correction stops being right the moment anything else appears on screen.

Testing

The test suite launches a headless FXGL game of its own, sets every graphic going in it, and measures them as they run. Run it from the package root:

aussom -t agrfun-test.aus