Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

hyperdrive effect in canvas across randomly placed circles

I'm trying to create a hyperdrive effect, like from Star Wars, where the stars have a motion trail. I've gotten as far as creating the motion trail on a single circle, it still looks like the trail is going down in the y direction and not forwards or positive in the z direction.

Also, how could I do this with (many) randomly placed circles as if they were stars?

My code is on jsfiddle (https://jsfiddle.net/5m7x5zxu/) and below:

var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");

var xPos = 180;
var yPos = 100;

var motionTrailLength = 16;
var positions = [];

function storeLastPosition(xPos, yPos) {
  // push an item
  positions.push({
    x: xPos,
    y: yPos
  });

  //get rid of first item
  if (positions.length > motionTrailLength) {
    positions.pop();
  }
}
function update() {
  context.clearRect(0, 0, canvas.width, canvas.height);

  for (var i = positions.length-1; i > 0; i--) {
    var ratio = (i - 1) / positions.length;
    drawCircle(positions[i].x, positions[i].y, ratio);
  }

  drawCircle(xPos, yPos, "source");
 var k=2;
  storeLastPosition(xPos, yPos);

  // update position
  if (yPos > 125) {
    positions.pop();
  }
  else{
    yPos += k*1.1;
   }

  requestAnimationFrame(update);
}
update();


function drawCircle(x, y, r) {
  if (r == "source") {
    r = 1;
  } else {
    r*=1.1;
  }

  context.beginPath();
  context.arc(x, y, 3, 0, 2 * Math.PI, true);
  context.fillStyle = "rgba(255, 255, 255, " + parseFloat(1-r) + ")";
  context.fill();
}
like image 649
wordSmith Avatar asked Oct 25 '25 14:10

wordSmith


1 Answers

Canvas feedback and particles.

This type of FX can be done many ways.

You could just use a particle systems and draw stars (as lines) moving away from a central point, as the speed increase you increase the line length. When at low speed the line becomes a circle if you set ctx.lineWidth > 1 and ctx.lineCap = "round"

To add to the FX you can use render feedback as I think you have done by rendering the canvas over its self. If you render it slightly larger you get a zoom FX. If you use ctx.globalCompositeOperation = "lighter" you can increase the stars intensity as you speed up to make up for the overall loss of brightness as stars move faster.

Example

I got carried away so you will have to sift through the code to find what you need.

The particle system uses the Point object and a special array called bubbleArray to stop GC hits from janking the animation.

You can use just an ordinary array if you want. The particles are independent of the bubble array. When they have moved outside the screen they are move to a pool and used again when a new particle is needed. The update function moves them and the draw Function draws them I guess LOL

The function loop is the main loop and adds and draws particles (I have set the particle count to 400 but should handle many more)

The hyper drive is operated via the mouse button. Press for on, let go for off. (It will distort the text if it's being displayed)

The canvas feedback is set via that hyperSpeed variable, the math is a little complex. The sCurce function just limits the value to 0,1 in this case to stop alpha from going over or under 1,0. The hyperZero is just the sCurve return for 1 which is the hyper drives slowest speed.

I have pushed the feedback very close to the limit. In the first few lines of the loop function you can set the top speed if(mouse.button){ if(hyperSpeed < 1.75){ Over this value 1.75 and you will start to get bad FX, at about 2 the whole screen will just go white (I think that was where)

Just play with it and if you have questions ask in the comments.

const ctx = canvas.getContext("2d");

// very simple mouse
const mouse  = {x : 0, y : 0, button : false}	
function mouseEvents(e){
	mouse.x = e.pageX;
	mouse.y = e.pageY;
	mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));

// High performance array pool using buubleArray to separate pool objects and active object.
// This is designed to eliminate GC hits involved with particle systems and 
// objects that have short lifetimes but used often.
// Warning this code is not well tested.
const bubbleArray = () => {
    const items = [];
    var count = 0;
    return {
        clear(){  // warning this dereferences all locally held references and can incur Big GC hit. Use it wisely.
            this.items.length = 0;
            count = 0;
        },
        update() {
            var head, tail;
            head = tail = 0;
            while(head < count){
                if(items[head].update() === false) {head += 1 }
                else{
                    if(tail < head){
                        const temp = items[head];
                        items[head] = items[tail];
                        items[tail] = temp;
                    }
                    head += 1;
                    tail += 1;
                }
            }
            return count = tail;
        },
        createCallFunction(name, earlyExit = false){
            name = name.split(" ")[0];
            const keys = Object.keys(this);
            if(Object.keys(this).indexOf(name) > -1){  throw new Error(`Can not create function name '${name}' as it already exists.`) }
            if(!/\W/g.test(name)){
                let func;
                if(earlyExit){
                    func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ if (items[i++].${name}() === true) { break } }`;
                }else{
                    func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ items[i++].${name}() }`;
                }
                !this.items && (this.items = items);
                this[name] = new Function(func);
            }else{  throw new Error(`Function name '${name}' contains illegal characters. Use alpha numeric characters.`) }
            
        },
        callEach(name){var i = 0; while(i < count){ if (items[i++][name]() === true) { break } } },
        each(cb) { var i = 0; while(i < count){ if (cb(items[i], i++) === true) { break } } },
        next() { if (count < items.length) { return items[count ++] } },
        add(item) {
            if(count === items.length){
                items.push(item);
                count ++;
            }else{
                items.push(items[count]);
                items[count++] = item;
            }
            return item;
        },
        getCount() { return count },
    }
}

// Helpers rand float, randI random Int
// doFor iterator
// sCurve curve input -Infinity to Infinity out -1 to 1
// randHSLA creates random colour
// CImage, CImageCtx create image and image with context attached
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand  = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove

const sCurve = (v,p) => (2 / (1 + Math.pow(p,-v))) -1;
const randHSLA = (h, h1, s = 100, s1 = 100, l = 50, l1 = 50, a = 1, a1 = 1) => { return `hsla(${randI(h,h1) % 360},${randI(s,s1)}%,${randI(l,l1)}%,${rand(a,a1)})` }
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);

// create image to hold text
var textImage = CImageCtx(1024, 1024);
var c = textImage.ctx;
c.fillStyle = "#FF0";
c.font = "64px arial black";
c.textAlign = "center";
c.textBaseline = "middle";
const text = "HYPER,SPEED FX,VII,,Battle of Jank,,Hold the mouse,button to increase,speed.".split(",");
text.forEach((line,i) => { c.fillText(line,512,i * 68 + 68) });
const maxLines = text.length * 68 + 68;
function starWarIntro(image,x1,y1,x2,y2,pos){
  var iw = image.width;
  var ih = image.height;
  var hh = (x2 - x1) / (y2 - y1);      // Slope of left edge
  var w2 = iw / 2;                      // half width
  var z1 = w2 - x1;                    // Distance (z) to first line
  var z2 = (z1 / (w2 - x2)) * z1 - z1; // distance (z) between first and last line
  var sk,t3,t3a,z3a,lines, z3, dd = 0, a = 0, as = 2 / (y2 - y1);
  for (var y = y1; y < y2 && dd < maxLines; y++) {      // for each line

      t3 = ((y - y1) * hh) + x1;       // get scan line top left edge
      t3a = (((y+1) - y1) * hh) + x1;  // get scan line bottom left edge
      z3 = (z1 / (w2 - t3)) * z1;      // get Z distance to top of this line
      z3a = (z1 / (w2 - t3a)) * z1;      // get Z distance to bottom of this line
      dd = ((z3 - z1) / z2) * ih;       // get y bitmap coord
      a += as;

      ctx.globalAlpha = a < 1 ? a : 1;
      dd += pos;                         // kludge for this answer to make text move
                                         // does not move text correctly 
      lines = ((z3a - z1) / z2) * ih-dd;       // get number of lines to copy
      ctx.drawImage(image, 0, dd , iw, lines, t3, y, w - t3 * 2, 1.5);
  }
}


// canvas settings
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center 
var ch = h / 2;
// diagonal distance used to set point alpha (see point update)
var diag = Math.sqrt(w * w + h * h);
// If window size is changed this is called to resize the canvas
// It is not called via the resize event as that can fire to often and
// debounce makes it feel sluggish so is called from main loop.
function resizeCanvas(){
  points.clear();
  canvas.width = innerWidth;
  canvas.height = innerHeight;
  w = canvas.width;
  h = canvas.height;
  cw = w / 2;  // center 
  ch = h / 2;
  diag = Math.sqrt(w * w + h * h);
  
}
// create array of points
const points = bubbleArray(); 
// create optimised draw function itterator
points.createCallFunction("draw",false);
// spawns a new star
function spawnPoint(pos){
    var p = points.next();
    p = points.add(new  Point())    
    if (p === undefined) { p = points.add(new  Point()) }
    p.reset(pos);  
}
// point object represents a single star
function Point(pos){  // this function is duplicated as reset 
    if(pos){
        this.x = pos.x;   
        this.y = pos.y;   
        this.dead = false;
    }else{
        this.x = 0;
        this.y = 0;
        this.dead = true;
    }
    this.alpha = 0;
    var x = this.x - cw;
    var y = this.y - ch;
    this.dir = Math.atan2(y,x);
    this.distStart = Math.sqrt(x * x + y * y);
    this.speed = rand(0.01,1);
    this.col = randHSLA(220,280,100,100,50,100);
    this.dx = Math.cos(this.dir) * this.speed;
    this.dy = Math.sin(this.dir) * this.speed;
}
Point.prototype = {
    reset : Point,  // resets the point
    update(){       // moves point and returns false when outside 
        this.speed *= hyperSpeed;  // increase speed the more it has moved
        this.x += Math.cos(this.dir) * this.speed;
        this.y += Math.sin(this.dir) * this.speed;
        var x = this.x - cw;
        var y = this.y - ch;
        this.alpha = (Math.sqrt(x * x + y * y) - this.distStart) / (diag * 0.5 - this.distStart);
        if(this.alpha > 1 || this.x < 0 || this.y < 0 || this.x > w || this.h > h){
           this.dead = true;
        }
        return !this.dead;
    },
    draw(){  // draws the point 
        ctx.strokeStyle = this.col;
        ctx.globalAlpha = 0.25 + this.alpha *0.75;
        ctx.beginPath();
        ctx.lineTo(this.x - this.dx * this.speed, this.y - this.dy * this.speed);
        ctx.lineTo(this.x, this.y);
        ctx.stroke();

    }
}

const maxStarCount = 400;
const p = {x : 0, y : 0};
var hyperSpeed = 1.001;
const alphaZero = sCurve(1,2);
var startTime;
function loop(time){

    if(startTime === undefined){
        startTime = time;
    }
    if(w !== innerWidth || h !== innerHeight){
       resizeCanvas();
    }
    // if mouse down then go to hyper speed
    if(mouse.button){
        if(hyperSpeed < 1.75){
            hyperSpeed += 0.01;
        }
    }else{
        if(hyperSpeed > 1.01){
            hyperSpeed -= 0.01;
        }else if(hyperSpeed > 1.001){
            hyperSpeed -= 0.001;
        }
    }
    
    var hs = sCurve(hyperSpeed,2);
    ctx.globalAlpha = 1;
    ctx.setTransform(1,0,0,1,0,0); // reset transform


    //==============================================================
    // UPDATE the line below could be the problem. Remove it and try
    // what is under that        
    //==============================================================
    //ctx.fillStyle = `rgba(0,0,0,${1-(hs-alphaZero)*2})`;
    
    // next two lines are the replacement
    ctx.fillStyle = "Black";
    ctx.globalAlpha = 1-(hs-alphaZero) * 2;
    //==============================================================



    ctx.fillRect(0,0,w,h);
    // the amount to expand canvas feedback
    var sx = (hyperSpeed-1) * cw * 0.1;
    var sy = (hyperSpeed-1) * ch * 0.1;

    // increase alpha as speed increases
    ctx.globalAlpha = (hs-alphaZero)*2;
    ctx.globalCompositeOperation = "lighter";
    // draws feedback twice
    ctx.drawImage(canvas,-sx, -sy, w + sx*2 , h + sy*2)
    ctx.drawImage(canvas,-sx/2, -sy/2, w + sx , h + sy)
    ctx.globalCompositeOperation = "source-over";
    
    // add stars if count < maxStarCount 
    if(points.getCount() < maxStarCount){
        var cent = (hyperSpeed - 1) *0.5; // pulls stars to center as speed increases
        doFor(10,()=>{
            p.x = rand(cw * cent ,w - cw * cent);  // random screen position
            p.y = rand(ch * cent,h - ch * cent);
            spawnPoint(p)
            
        })
    }
    // as speed increases make lines thicker
    ctx.lineWidth = 2 + hs*2;
    ctx.lineCap = "round";
    points.update();  // update points
    points.draw();     // draw points
    ctx.globalAlpha = 1;

    // scroll the perspective star wars text FX
    var scrollTime = (time - startTime) / 5 - 2312;
    if(scrollTime < 1024){
        starWarIntro(textImage,cw - h * 0.5, h * 0.2, cw - h * 3, h , scrollTime );
    }
	requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
like image 138
Blindman67 Avatar answered Oct 27 '25 06:10

Blindman67