Skip to content

SpriteShadows

Install

Please install the @canvasengine/presets package first.

bash
npm install @canvasengine/presets

Then, you can use the presets in your project.

Overview

SpriteShadows adds RPG-style ground shadows for sprites.

  • The real silhouette of each sprite is projected on the ground, away from each light: a torch on the left draws the character's shape stretched to the right.
  • The projection follows the light height (z): close low lights give long shadows, a high sun gives short ones.
  • The shadow is crisp at the feet and gets softer and lighter toward its tip (penumbra), with a soft contact shadow under the feet.
  • In multi mode (default) each nearby light casts its own shadow, and shadows fade in and out as characters walk between lights.
  • All shadows are drawn in one ground layer and merged before its opacity is applied: like in classic 2D RPGs, overlapping shadows keep one soft, even tone instead of stacking into dark streaks.
  • Shadow length is capped at 1.5× the caster height by default, so low lights never draw endless strokes.
  • Silhouettes are prepared once per texture frame (animated sprites included), not every frame, and no filter runs per shadow.

Pair it with sunShadowAt(hour) from DayNightCycle for shadows that follow the time of day.

To cast a shadow, a sprite must be tagged with shadowCaster.

Basic Usage

html
<Canvas>
  <Viewport worldWidth={2048} worldHeight={2048} sortableChildren={true}>
    <Sprite image="back.png" />

    <SpriteShadows
      lights={lights}
      ambientLight={{ x: -0.2, y: -1, z: 420, intensity: 0.18 }}
      minInfluence={0.16}
      falloffPower={1.2}
      mode="strongest"
      updateHz={60}
      scanHz={8}
      cullToViewport={true}
      shadowColor="#05070d"
    />

    <Sprite
      image="hero.png"
      x={heroX}
      y={heroY}
      anchor={{ x: 0.5, y: 0.86 }}
      shadowCaster={{
        height: 96,
        alpha: 0.6,
        blur: 4,
        gradientPower: 2.2,
        hardness: 0.45,
      }}
    />
  </Viewport>
</Canvas>

<script>
  import { computed, signal } from 'canvasengine'
  import { SpriteShadows } from '@canvasengine/presets'

  const heroX = signal(620)
  const heroY = signal(540)

  const lights = computed(() => [
    { x: heroX(), y: heroY() - 120, z: 230, radius: 360, intensity: 1.2 },
    { x: 760, y: 430, z: 170, radius: 300, intensity: 1.0 },
  ])
</script>

With SpriteSheet Animations

SpriteShadows also works with animated Sprite sheet={...}.

html
<Sprite
  x={heroX}
  y={heroY}
  sheet={{ definition: heroSheetDefinition, playing: 'walk' }}
  shadowCaster={{ height: 92, blur: 4, gradientPower: 2.1 }}
/>

Preset Props

PropTypeDefaultDescription
lightsArray<LightInput> | Signal<Array> | () => Array[]Main reactive light list
sourcesSame as lights[]Alias of lights
ambientLightAmbientLightInput | Signal<AmbientLightInput> | () => AmbientLightInputnullDirectional baseline light used when no point light reaches a caster
minInfluencenumber | Signal<number> | () => number0Minimum ambient influence. Use with ambientLight to keep distant sprites grounded
falloffPowernumber | Signal<number> | () => number2Point-light falloff curve. Lower values keep shadows visible farther from the source
mode'multi' | 'strongest' | 'blend2''multi'multi: one shadow per light; strongest: dominant light only; blend2: one shadow averaging the two strongest lights
maxShadowsnumber3Maximum shadows per caster in multi mode
updateHznumber | Signal<number> | () => number60Shadow recompute frequency
scanHznumber | Signal<number> | () => number8Scene scan frequency used to discover added, removed, or retagged shadow casters
cullToViewportboolean | Signal<boolean> | () => booleanfalseHide and skip shadow updates for casters outside the visible target bounds
shadowColorstring | number0x0a0c16Shadow tint
opacitynumber | Signal<number>0.5Opacity of the merged shadow layer (0.3 to 0.5 looks like most 2D RPGs)
layerZIndexnumberjust below the lowest casterzIndex of the shadow layer

LightInput

FieldTypeRequiredDefaultDescription
xnumber | Signal<number> | () => numberYes-Light X position
ynumber | Signal<number> | () => numberYes-Light Y position
znumber | Signal<number> | () => numberNo220Light height (higher = shorter shadow)
radiusnumber | Signal<number> | () => numberNo360Influence radius
intensitynumber | Signal<number> | () => numberNo1Influence multiplier (0..2)
shadowWeightnumber | Signal<number> | () => numberNo1Priority weight for shadow selection (useful for torches)
enabledboolean | Signal<boolean> | () => booleanNotrueEnable/disable source

AmbientLightInput

ambientLight is a directional source, useful for moonlight or a weak global fill. Its x and y fields describe where the light comes from. For example, { x: 0, y: -1 } means the light comes from above the scene, so the shadow projects downward.

html
<SpriteShadows
  lights={torches}
  ambientLight={{ x: -0.18, y: -1, z: 420, intensity: 0.18, shadowWeight: 0.75 }}
  minInfluence={0.16}
  falloffPower={1.2}
  mode="blend2"
/>
FieldTypeRequiredDefaultDescription
xnumber | Signal<number> | () => numberYes-Horizontal incoming light direction
ynumber | Signal<number> | () => numberYes-Vertical incoming light direction
znumber | Signal<number> | () => numberNo420Ambient source height (higher = shorter shadow)
intensitynumber | Signal<number> | () => numberNo0.28Baseline influence multiplier
shadowWeightnumber | Signal<number> | () => numberNo1Ambient priority when blended with point lights
lengthnumber | Signal<number> | () => numberNoderived from caster height and zFixed ambient shadow length
enabledboolean | Signal<boolean> | () => booleanNotrueEnable/disable ambient shadowing

shadowCaster Options

Attach on each sprite that must cast a shadow:

html
<Sprite
  image="npc.png"
  shadowCaster={{
    height: 88,
    footOffset: { x: 0, y: 4 },
    alpha: 0.55,
    blur: 3.6,
    gradientPower: 2.1,
    hardness: 0.5,
    minLength: 10,
    maxLength: 260,
    contactAlpha: 0.28,
    contactScale: 0.3,
  }}
/>
FieldTypeDefaultDescription
enabledbooleantrueEnable/disable this caster
heightnumber72Approximate sprite height used for projection
footOffset{ x, y }{ x: 0, y: 0 }Offset of ground contact point
footAnchor{ x, y }{ x: 0.5, y: 1 }Ground point inside sprite bounds
alphanumber1Shadow strength of this caster inside the layer
blurnumber3.5Blur strength
gradientPowernumber2Tail falloff. Higher values make the projected part shorter and fainter
hardnessnumber0.42Shadow sharpness (0..1)
minLengthnumber10Minimum projected length
maxLengthnumber1.5 × heightMaximum projected length
contactAlphanumber0.7Strength of the contact shadow at the feet (inside the layer)
contactScalenumber0.34Contact shadow ellipse size factor
anchorXnumbercaster anchor or 0.5Horizontal anchor override for silhouette
perspectivenumber0.7Ground foreshortening (1 = seen from straight above, 0.6 = RPG 3/4 view)
silhouettebooleantrueProject the sprite shape. false uses a soft blob instead

RPG Tuning Tips

  • For torches/candles: lower z and medium blur.
  • For moon/sun style: higher z, larger radius, lower intensity.
  • For night scenes: use ambientLight plus minInfluence so sprites keep a readable contact shadow outside spot radius.
  • Lower falloffPower (1.2..1.5) when local lights should fade more gradually.
  • multi looks the most natural around several lamps; use strongest for a single clean shadow per sprite.
  • Sprites should be drawn without a baked ground shadow, and anchored at their feet (anchor={[0.5, 1]}).
  • For a daylight scene, use ambientLight={sunShadowAt(hour)}: shadows sweep west to east and stay short at noon.
  • Increase updateHz for very responsive character movement; keep scanHz lower because caster discovery is more expensive.
  • Enable cullToViewport on large maps with many off-screen casters.