Как реализовать эффект (исчезающего следа) как в игре dark echo
Я хочу сделать эффект визуального звука, который отражается от стен и постепенно затухает (как в игре dark echo).
Каким образом я могу это сделать средствами canvas?

Проблема в следующем:
Если я буду делать через создание полупрозрачных копий частицы, то и это слишком будет тормозить отрисовку (след нужен довольно длинный)
Движение WASD или стрелки
Пробел - создать звук
<!DOCTYPE html>
<html>
<head>
<title>шлейф</title>
<meta charset="utf-8">
<style>
canvas {
border: 2px solid grey;
}
</style>
<script
src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous">
</script>
</head>
<body>
<canvas id="canvas" width="200" height="200"></canvas>
<script type="text/javascript">
var game_status = 'active';
var x_cam = 0;
var y_cam = 0;
var direction = {
"37": "left",
"38": "up",
"39": "right",
"40": "down",
"65": "left",
"87": "up",
"68": "right",
"83": "down",
};
var ctx = document.getElementById('canvas').getContext('2d');
var map = {
ctx: ctx,
width: 200,
height: 200,
particles: [],
circle: function (x, y, radius, color, fillCircle=true) {
this.ctx.beginPath();
this.ctx.arc(x, y, radius, 0, Math.PI * 2, false);
if (fillCircle) {
this.ctx.fillStyle = color;
this.ctx.fill();
} else {
this.ctx.strokeStyle = color;
this.ctx.stroke();
}
},
clear: function() {
this.ctx.clearRect(0, 0, this.width, this.height)
},
alpha_clear: function(alpha) {
this.ctx.fillStyle = "rgba(255, 255, 255,"+alpha+")"
this.ctx.fillRect(0, 0, this.width, this.height);
}
}
var player = {
x: 100,
y: 100,
speed: 2,
x_vec: 0,
y_vec: 0,
angle: 0,
direction: {
"up": false,
"down": false,
"right": false,
"left": false
},
setDirection: function(dir, status) {
if (dir == 'up' || dir == 'down' || dir == 'right' || dir == 'left') {
this.direction[dir] = status;
}
},
setVecs: function() {
this.x_vec = 0;
this.y_vec = 0;
if (this.direction.up) {
this.y_vec--;
}
if (this.direction.down) {
this.y_vec++;
}
if (this.direction.right) {
this.x_vec++;
}
if (this.direction.left) {
this.x_vec--;
}
if (Math.abs(this.x_vec) > 0 && Math.abs(this.y_vec) > 0) {
this.x_vec *= 0.7;
this.y_vec *= 0.7;
}
// строго наверх
if (this.x_vec == 0 && this.y_vec == -1) {
this.angle = 0
}
// строго вниз
if (this.x_vec == 0 && this.y_vec == 1) {
this.angle = Math.PI;
}
// строго вправо
if (this.x_vec == 1 && this.y_vec == 0) {
this.angle = Math.PI/2;
}
// строго влево
if (this.x_vec == -1 && this.y_vec == 0) {
this.angle = Math.PI*1.5;
}
// наверх-вправо
if (this.x_vec == 0.7 && this.y_vec == -0.7) {
this.angle = Math.PI/4;
}
// вниз-вправо
if (this.x_vec == 0.7 && this.y_vec == 0.7) {
this.angle = Math.PI*0.75;
}
// вниз-влево
if (this.x_vec == -0.7 && this.y_vec == 0.7) {
this.angle = Math.PI*1.25;
}
// вверх-влево
if (this.x_vec == -0.7 && this.y_vec == -0.7) {
this.angle = Math.PI*1.75;
}
},
draw: function() {
map.circle(this.x-x_cam, this.y-y_cam, 10, "red");
},
update: function() {
this.x += this.x_vec*this.speed;
this.y += this.y_vec*this.speed;
x_cam += this.x_vec*this.speed;
y_cam += this.y_vec*this.speed;
},
createSound: function() {
var angle_step = Math.PI*2/10;
for (var i=0; i < 10; i++) {
map.particles.push(new Particle(this.x, this.y, i*angle_step));
}
}
}
var Particle = function(x=300, y=300, angle=0) {
this.x= x;
this.y= y;
this.angle = angle;
this.x_vec= Math.cos(angle);
this.y_vec= Math.sin(angle);
this.speed = 5;
this.tail = [];
for (var i=0; i < 20; i++) {
this.tail.push({
"x": x,
"y": y,
})
}
this.draw= function() {
map.circle(this.x-x_cam, this.y-y_cam, 3, 'black');
for (var i=0; i < this.tail.length; i++) {
map.circle(this.tail[i].x-x_cam, this.tail[i].y-y_cam, 3, 'black');
}
};
this.update= function() {
this.x += this.x_vec*this.speed;
this.y += this.y_vec*this.speed;
}
this.update_tail = function() {
for (var j=this.tail.length-1; j > 0; j--) {
this.tail[j] = this.tail[j-1];
}
this.tail.pop();
this.tail.unshift({
"x": this.x,
"y": this.y,
})
//this.tail[0].x = this.x;
//this.tail[0].y = this.y;
}
}
$("body").keydown(function (event) {
//console.log(snake);
if (direction[event.keyCode] != undefined) {
var pl_direction = direction[event.keyCode];
player.setDirection(pl_direction, true);
} else {
if (event.keyCode== 13) {
if (game_status == 'active') {
game_status = 'pause';
} else {
game_status = 'active';
}
} else if (event.keyCode == 32) {
player.createSound();
}
}
});
$("body").keyup(function (event) {
//console.log(snake);
if (direction[event.keyCode] != undefined) {
var pl_direction = direction[event.keyCode];
player.setDirection(pl_direction, false);
}
});
var particle = new Particle();
var game_loop = function() {
if (game_status == 'active') {
window.requestAnimationFrame(game_loop);
map.clear();
ctx.fillStyle = "black";
ctx.fillRect(100-x_cam, 100-y_cam, 100, 100);
player.setVecs();
player.update();
for (var i in map.particles) {
map.particles[i].update_tail();
map.particles[i].update();
map.particles[i].draw()
}
particle.update_tail();
particle.update();
particle.draw();
player.draw();
}
}
game_loop();
</script>
</body>
</html>
Однако если я буду неполностью стирать
canvas, то во время движения линии будут искажаться (я для имитации viewport (движения камеры) отнимаю условную позицию камеры). Это хорошо видно, если стрелками туда-сюда двигаться
Движение WASD или стрелки
Пробел - создать звук
<!DOCTYPE html>
<html>
<head>
<title>шлейф</title>
<meta charset="utf-8">
<style>
canvas {
border: 2px solid grey;
}
</style>
<script
src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous">
</script>
</head>
<body>
<canvas id="canvas" width="200" height="200"></canvas>
<script type="text/javascript">
var game_status = 'active';
var x_cam = 0;
var y_cam = 0;
var direction = {
"37": "left",
"38": "up",
"39": "right",
"40": "down",
"65": "left",
"87": "up",
"68": "right",
"83": "down",
};
var ctx = document.getElementById('canvas').getContext('2d');
var map = {
ctx: ctx,
width: 200,
height: 200,
particles: [],
circle: function (x, y, radius, color, fillCircle=true) {
this.ctx.beginPath();
this.ctx.arc(x, y, radius, 0, Math.PI * 2, false);
if (fillCircle) {
this.ctx.fillStyle = color;
this.ctx.fill();
} else {
this.ctx.strokeStyle = color;
this.ctx.stroke();
}
},
clear: function() {
this.ctx.clearRect(0, 0, this.width, this.height)
},
alpha_clear: function(alpha) {
this.ctx.fillStyle = "rgba(255, 255, 255,"+alpha+")"
this.ctx.fillRect(0, 0, this.width, this.height);
}
}
var player = {
x: 100,
y: 100,
speed: 3,
x_vec: 0,
y_vec: 0,
angle: 0,
direction: {
"up": false,
"down": false,
"right": false,
"left": false
},
setDirection: function(dir, status) {
if (dir == 'up' || dir == 'down' || dir == 'right' || dir == 'left') {
this.direction[dir] = status;
}
},
setVecs: function() {
this.x_vec = 0;
this.y_vec = 0;
if (this.direction.up) {
this.y_vec--;
}
if (this.direction.down) {
this.y_vec++;
}
if (this.direction.right) {
this.x_vec++;
}
if (this.direction.left) {
this.x_vec--;
}
if (Math.abs(this.x_vec) > 0 && Math.abs(this.y_vec) > 0) {
this.x_vec *= 0.7;
this.y_vec *= 0.7;
}
// строго наверх
if (this.x_vec == 0 && this.y_vec == -1) {
this.angle = 0
}
// строго вниз
if (this.x_vec == 0 && this.y_vec == 1) {
this.angle = Math.PI;
}
// строго вправо
if (this.x_vec == 1 && this.y_vec == 0) {
this.angle = Math.PI/2;
}
// строго влево
if (this.x_vec == -1 && this.y_vec == 0) {
this.angle = Math.PI*1.5;
}
// наверх-вправо
if (this.x_vec == 0.7 && this.y_vec == -0.7) {
this.angle = Math.PI/4;
}
// вниз-вправо
if (this.x_vec == 0.7 && this.y_vec == 0.7) {
this.angle = Math.PI*0.75;
}
// вниз-влево
if (this.x_vec == -0.7 && this.y_vec == 0.7) {
this.angle = Math.PI*1.25;
}
// вверх-влево
if (this.x_vec == -0.7 && this.y_vec == -0.7) {
this.angle = Math.PI*1.75;
}
},
draw: function() {
map.circle(this.x-x_cam, this.y-y_cam, 10, "red");
},
update: function() {
this.x += this.x_vec*this.speed;
this.y += this.y_vec*this.speed;
x_cam += this.x_vec*this.speed;
y_cam += this.y_vec*this.speed;
},
createSound: function() {
var angle_step = Math.PI*2/10;
for (var i=0; i < 10; i++) {
map.particles.push(new Particle(this.x, this.y, i*angle_step));
}
}
}
var Particle = function(x=300, y=300, angle=0) {
this.x= x;
this.y= y;
this.angle = angle;
this.x_vec= Math.cos(angle);
this.y_vec= Math.sin(angle);
this.speed = 5;
this.tail = [];
for (var i=0; i < 20; i++) {
this.tail.push({
"x": x,
"y": y,
})
}
this.draw= function() {
map.circle(this.x-x_cam, this.y-y_cam, 3, 'black');
/*for (var i=0; i < this.tail.length; i++) {
map.circle(this.tail[i].x-x_cam, this.tail[i].y-y_cam, 3, 'black');
}*/
};
this.update= function() {
this.x += this.x_vec*this.speed;
this.y += this.y_vec*this.speed;
}
this.update_tail = function() {
for (var j=this.tail.length-1; j > 0; j--) {
this.tail[j] = this.tail[j-1];
}
this.tail.pop();
this.tail.unshift({
"x": this.x,
"y": this.y,
})
//this.tail[0].x = this.x;
//this.tail[0].y = this.y;
}
}
$("body").keydown(function (event) {
//console.log(snake);
if (direction[event.keyCode] != undefined) {
var pl_direction = direction[event.keyCode];
player.setDirection(pl_direction, true);
} else {
if (event.keyCode== 13) {
if (game_status == 'active') {
game_status = 'pause';
} else {
game_status = 'active';
}
} else if (event.keyCode == 32) {
player.createSound();
}
}
});
$("body").keyup(function (event) {
//console.log(snake);
if (direction[event.keyCode] != undefined) {
var pl_direction = direction[event.keyCode];
player.setDirection(pl_direction, false);
}
});
var particle = new Particle();
var game_loop = function() {
if (game_status == 'active') {
window.requestAnimationFrame(game_loop);
map.alpha_clear(0.15);
ctx.fillStyle = "black";
ctx.fillRect(100-x_cam, 100-y_cam, 100, 100);
player.setVecs();
player.update();
for (var i in map.particles) {
//map.particles[i].update_tail();
map.particles[i].update();
map.particles[i].draw()
}
//particle.update_tail();
particle.update();
particle.draw();
player.draw();
}
}
game_loop();
</script>
</body>
</html>
Мне обязательно нужно имитировать
viewport, т.к. вся карта не помещается в экранеВопрос: как сделать такой эффект, чтобы он не был сильно тяжёлым и не тормозил бразуер, но при этом выглядел достойно (не как в code shippet №2)