Cauldron2D.Projection.Volume (Cauldron2D v0.2.2)

Copy Markdown View Source

A coloured region of space that tints whatever is seen through it, and the arithmetic a projection draws one with.

volume = Cauldron2D.Projection.Volume.new(
  at: {8.5, 6.5},
  shape: {:sphere, 1.5, 0.0},
  colour: {120, 232, 96},
  strength: 0.6
)

:at is where the volume stands on the ground and :shape what it fills from there, as Cauldron2D.Projection.Volume.Shapes lists the shapes the engine knows, or {YourModule, data} for a shape of your own implementing Cauldron2D.Projection.Volume.Shape. Distances are in tiles. Nothing below the ground is drawn, whatever the shape says.

:strength is how much of the colour a viewer sees through the thickest part of the shape, 0.0 to 1.0. :falloff decides what a thinner crossing looks like:

  • :depth — the colour builds up along the ray, so the volume reads as something you look through: thin at the edges, full in the middle. The default
  • :flat — anything the ray touches at all is tinted by the full strength, which is how an area marked out in whole squares reads
  • :shell — strongest where the ray only grazes the shape, so the surface glows and the middle stays clear

A volume is never fully opaque.

A volume either stands where it was made or follows something that moves. :follows names what it keeps up with — a term of the caller's choosing, a character, a thrown light, itself — and anchored/2 moves every such volume to where its subject is now, turning a cone or a beam to face the way the subject faces and dropping a volume whose subject is gone. A volume with no :follows is left where it stands. Which effects follow what, and what may be followed at all, is the game's business; all this knows is that a subject has a place and may have a heading.

through/4 measures a ray against a volume and mix/3 blends that much colour into a pixel; the two together tint one pixel:

Volume.mix(pixel, volume, Volume.through(volume, eye, ray, distance_to_surface))

A ray is a point and a direction of three numbers, z running up from the ground, and how far along it the projection can see, or :infinity where nothing stops it. That limit counts steps of the direction as it is given, so a projection may measure it however suits it; the length through/4 answers is always in the units the volume itself is measured in, whatever size the direction is.

eyeline/3 gives the direction the rays of an axonometric projection all run in, box/2 the rectangle of a picture a volume can reach and reach/1 how far from at it spreads across the ground.

paint/4 tints a Cauldron2D.Projection.Canvas over a rectangle. It takes a plan rather than a ray per pixel, because a projection's rays change by the same step from one pixel to the next: ground_plan/2 describes a projection whose rays start on the ground and run parallel, eye_plan/3 one whose rays start at the eye and fan out.

Painting is done by Cauldron2D.Projection.Volume.Native where it built, and in Elixir where it did not or where the shape is a module of the caller's own. A plan carrying native: false paints in Elixir wherever it is, which is how the two are checked against each other; they agree to within one level of one channel, where :math.exp/1 and C's exp round differently.

Summary

Functions

Every volume moved to where the thing it follows is now.

The whole-pixel rectangle {left, top, right, bottom} of a picture the volume can reach, or nil when none of it is in view.

The world points of the box the volume fits in, above the ground.

A plan for a projection whose rays all start at the eye and fan out.

The direction, towards the viewer, that every ray of an axonometric projection runs in.

A plan for a projection whose rays start on the ground and all run the same way.

Every volume of a list, as new/1 builds one.

pixel with as much of the volume's colour blended in as length inside it calls for, by its falloff.

A volume from its fields, a map or a keyword list, with :at required.

Tint canvas with volume over box, clipped to the canvas, looking along plan.

How far from at the volume can spread across the ground.

How far a ray travels inside volume before limit, in the units the volume is measured in.

Types

falloff()

@type falloff() :: :depth | :flat | :shell

plan()

@type plan() :: %{
  :origin => point(),
  :origin_x => point(),
  :origin_y => point(),
  :direction => point(),
  :direction_x => point(),
  :direction_y => point(),
  :limits => binary() | nil,
  optional(:native) => boolean()
}

point()

@type point() :: {number(), number(), number()}

shape()

@type shape() :: tuple()

t()

@type t() :: %{
  at: {number(), number()},
  shape: {module(), term()},
  colour: {byte(), byte(), byte()},
  strength: float(),
  falloff: falloff(),
  thickness: float(),
  density: float()
}

Functions

anchored(volumes, subjects)

@spec anchored([map()], %{
  required(term()) => {number(), number()} | {{number(), number()}, number()}
}) ::
  [map()]

Every volume moved to where the thing it follows is now.

subjects maps what a volume's :follows names to where that subject is: a point, or {point, heading} where the subject faces somewhere and the volume's shape should turn with it. A volume with no :follows is returned untouched; one whose subject is absent from subjects is dropped, so an effect outlives nothing it was tied to.

box(volume, project)

@spec box(t(), (point() -> {number(), number()} | nil)) ::
  {integer(), integer(), integer(), integer()} | nil

The whole-pixel rectangle {left, top, right, bottom} of a picture the volume can reach, or nil when none of it is in view.

project maps a world point to its place in the picture, or to nil for a point the projection cannot show.

corners(map)

@spec corners(t()) :: [point()]

The world points of the box the volume fits in, above the ground.

eye_plan(from, along, limits \\ nil)

@spec eye_plan(point(), ({number(), number()} -> point()), binary() | nil) :: plan()

A plan for a projection whose rays all start at the eye and fan out.

along answers the direction a picture pixel looks in, scaled so that one step along it is one unit of depth, which is what limits counts. limits is one native float per picture pixel, how far that pixel sees before something opaque stops it, or nil for nothing in the way.

eyeline(arg1, arg2, lift)

@spec eyeline({number(), number()}, {number(), number()}, number()) :: point()

The direction, towards the viewer, that every ray of an axonometric projection runs in.

across and down are the picture vectors a step of one along the world's x and y moves by, and lift how many pixels up the picture a step of one in z moves. The three must not be flat against one another.

ground_plan(at, direction)

@spec ground_plan(({number(), number()} -> {number(), number()}), point()) :: plan()

A plan for a projection whose rays start on the ground and all run the same way.

at answers the world point on the ground under a picture pixel, as a projection's own unproject does, and direction is the way the rays run, as eyeline/3 gives it.

list(volumes)

@spec list([keyword() | map()]) :: [t()]

Every volume of a list, as new/1 builds one.

mix(pixel, volume, length)

@spec mix(binary(), t(), number()) :: binary()

pixel with as much of the volume's colour blended in as length inside it calls for, by its falloff.

A length of nothing leaves the pixel untouched. The pixel's own opacity byte is kept.

new(fields)

@spec new(keyword() | map()) :: t()

A volume from its fields, a map or a keyword list, with :at required.

:shape defaults to a dome of one tile, :colour to white, :strength to 0.5 and :falloff to :depth. Passing a volume back in leaves it as it is, so a projection may normalise whatever it is handed.

paint(canvas, volume, arg3, plan)

Tint canvas with volume over box, clipped to the canvas, looking along plan.

reach(map)

@spec reach(t()) :: float()

How far from at the volume can spread across the ground.

through(map, arg, direction, limit)

@spec through(t(), point(), point(), number() | :infinity) :: float()

How far a ray travels inside volume before limit, in the units the volume is measured in.

The ray starts at origin and runs along direction, and limit is how many steps of that direction it may take, so a projection whose step is a unit of depth rather than a unit of distance passes its own depth. Only what lies above the ground, ahead of the origin and nearer than limit counts; :infinity lets the ray run until it leaves the shape.