Skip to content

Add Trail renderable for ribbon/motion trail effects #1368

@obiot

Description

@obiot

Summary

Add a Trail renderable that draws a fading, tapering ribbon behind a moving object. This is one of the most common visual effects in 2D games — sword slashes, dash speed lines, bullet trails, magic arcs, cursor trails — and currently requires developers to build it from scratch.

Use Cases

  • Weapon slashes — short lifetime, wide, white or colored
  • Dash/speed lines — behind a character mid-dash
  • Bullet/projectile trails — thin, long lifetime
  • Magic/spell arcs — textured, colorful
  • Touch/cursor trails — follows pointer input
  • Moving platform indicators — shows recent path

API Sketch

Auto-follow mode — attach to a target, samples position every frame:

import { Trail } from "melonjs";

const trail = new Trail({
    target: player,          // sample position from this renderable
    length: 20,              // max points in buffer
    lifetime: 300,           // each point lives 300ms
    minDistance: 4,           // skip point if moved less than 4px (avoids clustering)
    widthStart: 12,          // px width at head
    widthEnd: 0,             // tapers to nothing at tail
    colorStart: "#44aaff",   // opaque at head
    colorEnd: "#44aaff00",   // transparent at tail
});
container.addChild(trail);

Manual mode — add points from game logic (e.g., sword tip during attack):

const slash = new Trail({ lifetime: 200, widthStart: 24, widthEnd: 2 });
container.addChild(slash);

// called each frame during the attack animation
slash.addPoint(swordTip.x, swordTip.y);

Textured ribbon — stretch a texture along the trail (fire, lightning, energy beam):

const beam = new Trail({
    target: projectile,
    texture: assets.getImage("beam_texture"),
    lifetime: 500,
    widthStart: 16,
    widthEnd: 8,
});

Implementation

Data Structure

A ring buffer of trail points. Each point stores { x, y, age }. On each update(dt):

  1. If a target is set, sample target.pos — skip if distance from last point < minDistance
  2. Push new point into the ring buffer
  3. Age all points by dt, remove any where age > lifetime

Rendering — Triangle Strip Tessellation

For each consecutive pair of points:

  1. Compute the perpendicular (normal) vector to the segment direction
  2. Scale it by the interpolated width (widthStart → widthEnd based on age / lifetime)
  3. Emit two vertices (left and right of center line) with interpolated color/alpha
  4. Connect as a triangle strip (each new pair of vertices forms two triangles with the previous pair)

UVs for textured mode are straightforward: U maps 0→1 along the trail length, V maps 0→1 across the width.

Both Renderers — Full Feature Parity

WebGL renderer: draw the triangle strip via the existing primitive batcher (untextured) or mesh batcher (textured).

Canvas renderer: use the same per-triangle affine texture mapping technique already used by drawMesh() — clip to triangle path, compute UV-to-screen transform, drawImage. For untextured trails, draw as line segments with interpolated lineWidth and strokeStyle.

No renderer-specific limitations — both get tapering width, color/alpha fading, textured ribbons, and all blend modes.

Class Design

Trail extends Renderable:

Parameters

Parameter Type Default Description
target Renderable | Vector2d null Auto-follow target
length number 20 Max points in buffer
lifetime number 500 Point lifetime in ms
minDistance number 4 Min px between samples
widthStart number 10 Width at head (px)
widthEnd number 0 Width at tail (px)
colorStart Color | string "#ffffff" Color at head
colorEnd Color | string "#ffffff00" Color at tail
texture HTMLImageElement null Optional texture for ribbon mode

Optional Enhancements (future)

  • Curve smoothing — Catmull-Rom interpolation between points for smoother curves at low sample rates
  • Width/color curves — arbitrary curves instead of linear start→end interpolation

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions