cast_shadow
Cast a pixel-art ground shadow away from a light source, built from palette ramp steps so colors stay conformant in indexed exports and tilesets.
Instructions
Lay a subject's shadow on the ground, away from the light and made of ramp steps.
`add_drop_shadow` offsets a copy of the artwork and tints it, which is a sticker of
the subject floating beside it. A cast shadow is a different thing: it falls on a
*surface*, away from the light, and flattens as it goes, so on the ground it is a
foreshortened ellipse under the subject rather than a second copy of its silhouette.
Built entirely of ramp steps, which is the point. A shadow made by multiplying alpha
lands every pixel of it between palette entries, and then `palette_conformance` drops
and nothing downstream holds together: indexed export, tileset reuse, a consistent
look between two sprites. The core is `ramp[0]` and each pixel of `softness` around it
is one step lighter, so the whole effect is ramp entries arranged in space.
Where it falls: the direction is away from `light_angle`, and the length comes from
`light_height` as the actual cotangent of the light's elevation. An overhead light
casts an ellipse straight underneath; a low light throws it far to one side.
Onto what: `ground_layer`. Name the layer holding the floor and the shadow is clipped
to it, so it cannot run off the edge of a platform and hang in the air, and if that
layer has nothing where the shadow would land the call is refused rather than drawing
a shadow onto nothing.
Args:
layer: The layer casting the shadow. Its cel is never modified. A background
layer is refused: it is opaque and covers the canvas, so it has no silhouette
to cast. `convert_background_to_layer` first if the subject is one.
ramp: Colours darkest first, normally the *ground's* ramp rather than the
subject's, since the shadow is a darkening of the surface it lies on.
Required, and deliberately so: a shadow built out of alpha instead is
`add_drop_shadow`, which already exists.
light_angle: Degrees. 0 is from the right, 90 from above, 135 from the upper left.
The shadow falls the opposite way.
light_height: The light's elevation, above 0 and up to 1. 1.0 is directly
overhead and casts no length at all; small values are a low sun and throw a
long shadow. This is the control that changes the shadow's length.
ground_y: The row the shadow lies on. Defaults to the subject's own contact row,
the lowest row it has a pixel on, which is the same measurement
`validate_loop` reports as `contact_rows`. Refused if it sits above that row,
because a floor running through the subject is not a floor.
ground_layer: The layer holding the surface. When given, the shadow is clipped to
that layer's pixels and the call is refused if there is nothing there to
catch it. Consulted for its pixels, not for whether the picture shows them: a
hidden layer, or one at a low opacity, still holds the pixels the clip asks
about, so the shadow is drawn and the result reports what the floor actually
is (see below) rather than the call being refused over it.
softness: Pixels of penumbra around the core, each one ramp step lighter. 0 is a
hard-edged shadow, 1 or 2 is the usual soft contact.
opacity: The shadow *layer's* opacity, 0 to 255. Left at 255 the shadow's pixels
are exactly ramp entries, which is what keeps conformance at 1.0; lowering it
blends them with whatever is underneath and takes the composite off the ramp,
so prefer a lighter ramp step over a lower opacity.
new_layer: Name for the shadow's own layer, created directly below `layer`, or
reused when a previous call of this tool made it and `frame` has no cel on it
yet. That is how one shadow layer carries a whole walk cycle. A second call for
the same layer *and* frame is refused, because two effects composited into one
cel are a picture neither call describes, and so is a layer of that name this
tool did not create.
frame: Frame to build the shadow for, 1-based.
Returns the ellipse it used as `shadow_ellipse` (`[cx, cy, rx, ry]`), the
`contact_row` it measured, and `shadow_pixels`. A shadow that landed entirely off the
canvas is refused rather than reported as a success that drew nothing.
With a `ground_layer` the result also says what that floor is: `ground_layer_visible`,
`ground_layer_opacity`, and `ground_layer_hidden` naming the layer and any group whose
flag is off, which is present only when something is. A floor the picture does not
show, or shows faintly, is reported in `warnings` as well, because a crisp shadow on
an invisible surface is usually a layer named by mistake rather than an intent.
`clipped_pixels` is routinely large next to `shadow_pixels` when `ground_layer` is a
thin floor, and that is arithmetic rather than a fault: the ellipse is centred on the
contact row and so half of it lies above the floor's top edge, where there is no
surface. Pass `ground_y` at the floor's own top row to push it down, or leave
`ground_layer` out and let the subject hide the upper half.
A shadow is refused when rasterising it would allocate more points than the limit,
which is reachable from legal arguments on a large canvas: the ellipse's radii grow
with the subject's size and with how low the light sits, and the point list is built
in full before any pixel is drawn, so that cost is memory rather than patience. The
message names both radii and the remedy. `ellipse_points` in the result says how close
an accepted shadow came.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| ramp | Yes | ||
| frame | No | Default: 1. | |
| layer | Yes | ||
| opacity | No | Default: 255. | |
| filename | Yes | ||
| ground_y | No | ||
| softness | No | Default: 1. | |
| new_layer | No | Default: "shadow". | shadow |
| light_angle | No | Default: 135.0. | |
| ground_layer | No | ||
| light_height | No | Default: 0.6. |