Vertex Trail with fading alpha possible?

Hi I have a visual trail. A vertex from history vector positions.
I want this trail to be transitioning from full alpha at the beginning and 0 alpha at its end.

When I do this with individual ellipses for the history array it works but vertex seems to only allow 1 alpha value.

The ellipse version does not look like a trail.

How can I achieve a vertex with a smooth alpha transition?

Here is my code. The important part is in the show function. When I comment out the vertex and enable the ellipse part (currently commented out) The opacity does what it should but it looks to rough with the ellipses:

let vehicle;
let maxSpeed = 20;
let maxForce = 20;
let slowRadius = 80;
let sliders = [];
let target;
let targetRadius;
let waveSpeedSlider;
let waveCheckbox;
let minHeight = 8;
let maxHeight = 40;
let alpha = 255;
function setup() {
  createCanvas(windowWidth, windowHeight);

  vehicle = new Vehicle(
    createVector(width / 2, height / 2, 0),
    maxSpeed,
    maxForce,
    slowRadius
  );

  target = {
    pos: createVector(width / 2, height / 2.5, 0),
    isBeingDragged: false,
  };

  sliders.push(createSlider(0, 40, maxSpeed, 1));
  sliders.push(createSlider(1, 40, maxForce, 1));
  sliders.push(createSlider(1, 500, slowRadius, 10));
  sliders.push(createSlider(0, height, height / 2, 1));

  for (let i = 0; i < sliders.length; i++) {
    sliders[i].position(50, 50 + i * 50);
    sliders[i].style("width", "200px");
    sliders[i].addClass("mySliders");
    sliders[i].input(updateValues);
  }

  let sliderLabels = ["Max Speed", "Max Force", "Slow Down Radius", "Target Z"];
  for (let i = 0; i < sliders.length; i++) {
    let label = createP(sliderLabels[i]);
    label.position(50, 50 + i * 50);
    label.style("color", "white");
  }

  waveSpeedSlider = createSlider(0, 0.1, 0.01, 0.001);
  waveSpeedSlider.position(50, 50 + sliders.length * 50);
  waveSpeedSlider.style("width", "200px");

  waveCheckbox = createCheckbox(
    "Use square mod = abrupt height change target",
    false
  );
  waveCheckbox.position(50, 70 + sliders.length * 50);
  waveCheckbox.style("color", "white");

  let targetLabel = createP("target = Red");
  targetLabel.position(50, 100 + sliders.length * 50);
  targetLabel.style("color", "rgb(248,63,82)");

  let vehicleLabel = createP("vehicle/follower = Blue");
  vehicleLabel.position(50, 130 + sliders.length * 50);
  vehicleLabel.style("color", "rgb(2,158,211)");

  let listenerLabel = createP("Listener");
  listenerLabel.position(width / 2 - 25, height / 2 + 5);
  listenerLabel.style("color", "white");
}

function draw() {
  background(50);

  let waveSpeed = waveSpeedSlider.value();

  let waveValue;
  if (waveCheckbox.checked()) {
    waveValue = (floor(frameCount * waveSpeed) % 2) * 2 - 1;
  } else {
    waveValue = sin(frameCount * waveSpeed);
  }

  let mappedWaveValue = map(waveValue, -1, 1, 0, height);
  sliders[3].value(mappedWaveValue);
  target.pos.z = mappedWaveValue;

  targetRadius = map(target.pos.z, 0, height, minHeight, maxHeight);

  fill(248, 63, 82);
  ellipse(target.pos.x, target.pos.y, targetRadius);

  noStroke();
  fill(255, 30);
  ellipse(target.pos.x, target.pos.y, slowRadius * 2);

  let d = dist(mouseX, mouseY, target.pos.x, target.pos.y);
  if (d < targetRadius) {
    cursor(HAND);
  } else {
    cursor(ARROW);
  }

  let steering = vehicle.arrive(target);
  vehicle.applyForce(steering);
  vehicle.update();
  vehicle.show();

  fill(255);
  ellipse(width / 2, height / 2, 20);

  textAlign(RIGHT, TOP);
  fill(2, 158, 211);
  text(
    `Vehicle Position - X: ${vehicle.pos.x.toFixed(
      2
    )}, Y: ${vehicle.pos.y.toFixed(2)}, Z: ${vehicle.pos.z.toFixed(2)}`,
    width - 20,
    20
  );
  fill(248, 63, 82);
  text(
    `Target Position - X: ${target.pos.x.toFixed(2)}, Y: ${target.pos.y.toFixed(
      2
    )}, Z: ${target.pos.z.toFixed(2)}`,
    width - 20,
    40
  );
}

function mousePressed() {
  let d = dist(mouseX, mouseY, target.pos.x, target.pos.y);
  if (d < targetRadius) {
    target.isBeingDragged = true;
  }
}

function mouseDragged() {
  if (target.isBeingDragged) {
    target.pos.x = mouseX;
    target.pos.y = mouseY;
    target.pos.z = sliders[3].value();
  }
}

function mouseReleased() {
  target.isBeingDragged = false;
}

function updateValues() {
  maxSpeed = sliders[0].value();
  maxForce = sliders[1].value();
  slowRadius = sliders[2].value();
  target.pos.z = sliders[3].value();

  vehicle.maxSpeed = maxSpeed;
  vehicle.maxForce = maxForce;
  vehicle.slowRadius = slowRadius;
}

class Vehicle {
  constructor(pos, maxSpeed, maxForce, slowRadius) {
    this.pos = pos;
    this.vel = createVector(0, 0, 0);
    this.acc = createVector(0, 0, 0);
    this.maxSpeed = maxSpeed;
    this.maxForce = maxForce;
    this.slowRadius = slowRadius;
    this.history = [];
    this.alpha = alpha;
  }

  arrive(target) {
    let desired = p5.Vector.sub(target.pos, this.pos);
    let d = desired.mag();
    desired.normalize();
    if (d < this.slowRadius) {
      let m = map(d, 0, this.slowRadius, 0, this.maxSpeed);
      desired.mult(m);
    } else {
      desired.mult(this.maxSpeed);
    }
    let steer = p5.Vector.sub(desired, this.vel);
    steer.limit(this.maxForce);
    return steer;
  }

  applyForce(force) {
    this.acc.add(force);
  }

  update() {
    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.acc.mult(0);

    var v = createVector(this.pos.x, this.pos.y);
    this.history.push(v);
    if (this.history.length > 20) {
      this.history.splice(0, 1);
      //      this.alpha -= 10;
    }
  }

  show() {
    // Draw the vehicle
    push();
    translate(this.pos.x, this.pos.y);
    let radius = map(this.pos.z, 0, height, minHeight, maxHeight);
    fill(2, 158, 211);
    ellipse(0, 0, radius);
    pop();

    // Draw the trail
    push();
    beginShape();
    let numOfSegments = this.history.length;
    for (let i = 0; i < numOfSegments; i++) {
      let pos = this.history[i];
      let segmentAlpha = map(i, 0, numOfSegments - 1, 0, 200); // Decreases from 255 to 50

      noFill();
      strokeWeight(radius);
      stroke(2, 158, 211, segmentAlpha);
      vertex(pos.x, pos.y);

      //           fill(2, 158, 211,segmentAlpha);
      //           ellipse(pos.x, pos.y, radius);
    }
    endShape();
    pop();
  }
}

I assume that beginShape() - endShape() creates a single shape object and it shares color, stroke and other values.

You could use line() function to draw your trail. Each line is independent element and you can change their color and transparency. As you have end points already in vectors it’s not a big change

    noFill();
    strokeWeight(radius);

    for (let i = 1; i < numOfSegments; i++) {
      let posStart = this.history[i-1];
      let posEnd = this.history[i];
      let segmentAlpha = map(i, 0, numOfSegments - 1, 0, 200); // Decreases from 255 to 50

      stroke(2, 158, 211, segmentAlpha);
      line(posStart.x, posStart.y, posEnd.x, posEnd.y);
    }
1 Like

Hello @Tilman,

Here is one approach with Processing Java version:

// Gradient Study - Fading Tail
// GLV
// v1.0.0 

size(800, 300, P2D); // Looks better with P2D
background(64);

// Generate an array of PVectors for lines
int s = 10;
PVector [] v = new PVector [s];

for(int i=0; i<s; i++)
  {
  v[i] = new PVector(i, 0);    
  }

float a1=0; 
float sc = (width-50.0)/s;

for(int i=1; i<s; i++)
  {
  a1 = map(i, 1, s-1, 15, 255); // alpha mapping from 15 to 255 for each segment
  println(a1);
   
  // Top line with blending
 push();
  translate(50, 100);
  strokeWeight(20);
  stroke(255, 255, 0, a1);
  line(v[i-1].x*sc, v[i-1].y,  v[i].x*sc, v[i].y);   
 pop();
  
 // Bottom line with no blending  
 push();
  translate(50, 200);
  
  float p0x = v[i-1].x*sc;
  float p0y = v[i-1].y*sc;
  
  float p1x = v[i].x*sc;
  float p1y = v[i].y*sc;
  
  strokeWeight(20);
  stroke(64);
  line(p0x, p0y,  p1x, p1y); // Same as background!   
  
  //strokeWeight(20);
  stroke(255, 255, 0, a1);
  line(p0x, p0y,  p1x, p1y);
 pop();
  }

s=10 :

s = 30 :

Modified Follow 3 example with P2D:

image

This can be adapted and works with p5.js as well!

:)