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();
}
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.
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With