So I am trying to make a exact smaller scale version of my canvas and sprites in p5.js and fit it in a box. Is there a function or way I can do this? The background, colors, and images of the sprites should be the same.
I don't know how p5.play works, but I'm guessing that the copy() command (reference) will do what you're looking for. To put the minimap in the bottom corner, you might do something like this:
function draw() {
//whatever is currently in your draw loop...
let minimapWidth = 50;
let minimapHeight = 50;
copy(0, 0, width, height,
width-minimapWidth,
height-minimapHeight,
minimapWidth, minimapHeight
);
}
There are two ways to do this, one way is to draw everything to a p5.Graphics buffer and the draw that buffer to your main canvas twice with different destination sizes. The other way is to do the main part of your drawing directly to your canvas, and then use the pixels array to create a p5.Image from the content of your canvas and then draw that to the canvas with the image function.
Example 1. p5.Graphics
let graphics;
function setup() {
createCanvas(windowWidth * 0.9, windowHeight * 0.9);
graphics = createGraphics(width, height);
graphics.background(100);
}
function draw() {
graphics.ellipse(mouseX, mouseY, 50, 50);
image(graphics, 0, 0, width, height, 0, 0, graphics.width, graphics.height);
// Draw picture in picture
let aspect = width / height;
image(graphics, 10, 10, 100, 100 / aspect, 0, 0, graphics.width, graphics.height);
push();
noFill();
strokeWeight(3);
rect(10, 10, 100, 100 / aspect);
pop();
}
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
</body>
</html>
Example 2. pixels & image
Note: this option has the disadvantage of being more complicated, waaay slower, and it doesn't support persistently drawn content as well as the p5.Graphics option because it displays itself on subsequent frames.
let img;
let density;
function setup() {
createCanvas(round(windowWidth * 0.9), round(windowHeight * 0.9));
density = pixelDensity();
img = createImage(width, height);
img.loadPixels();
background(100);
}
function draw() {
ellipse(mouseX, mouseY, 30, 30);
loadPixels();
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
let srcPixel = y * 4 * width * density ** 2 + x * 4 * density;
let dstPixel = y * 4 * img.width + x * 4;
for (let channel = 0; channel < 4; channel++) {
img.pixels[dstPixel + channel] = pixels[srcPixel + channel];
}
}
}
img.updatePixels();
// Draw picture in picture
let aspect = width / height;
image(img, 10, 10, 100, 100 / aspect, 0, 0, img.width, img.height);
push();
noFill();
strokeWeight(3);
rect(10, 10, 100, 100 / aspect);
pop();
}
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
</body>
</html>
Related
I am drawing rectangle on the corners of the image. For that, I am using JavaScript. Here I am getting the image data from canvas.
// Get image data
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let arr = imgData .data;
I am finding n which is the length of the adjacent and opposite of the triangle.
var n;
if (width > height) {
n = height / 20;
} else {
n = width / 20;
}
Here, I am using these loops to draw triangle with red color in background on left top corner of the image.
for (let y = 0; y < n; y++) {
for (i = y * imgData.width * 4; i < ((y * imgData.width * 4) + (n * 2)) - y; i = i + 4) {
arr[i] = 255;
arr[i + 1] = 0;
arr[i + 2] = 0;
}
}
Here is the output I am getting.
My expected output.
[
I am stuck with first triangle on left top corner of the image. I tried to debug the loops but did not able to find mistake. Can you please help me to find the mistake?
Image Reference:
http://webgl-workshop.com/assets/e826db271aa3c03c69c4aca1e20abf5b.jpg
Here is how you write the code to complete this drawing task
In .html, refer to your canvas with id="canvas"
In .js file, enter below code. It draw top left corner. Do similarly for other 3 corners.
Let me know if you succeed. Enjoy :) !
var canvasElement = document.querySelector("#canvas");
var ctx = canvasElement.getContext("2d");
var n = 50; //default at 50px edge length
var triangle = {
x1: 0,
y1: 0,
x2: 50,
y2: 0,
x3: 0,
y3: 50
}
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(triangle.x1, triangle.y1);
ctx.lineTo(triangle.x2, triangle.y2);
ctx.lineTo(triangle.x3, triangle.y3);
ctx.closePath();
ctx.stroke();
// the fill color
ctx.fillStyle = "#FF0000"; //or 'red'
ctx.fill();
I stubled upon a weird problem. The following code results in making the image fade away because it's overdrawn by a semi-opaque rect over and over again.
But at least at the 10th iteration of draw(); the image should be completely overdrawn, because the rect should be fully opaque by then, right? But it actually never disappears completely.
This effect is worse on Chrome than it is on Firefox. But beware: bad screens may hide this faulty behaviour =)
I also made a demo on jsFiddle.
$(function () {
var canvas = $("#mycanvas"),
ctx = canvas[0].getContext("2d"),
imgUrl = "http://it-runde.de/dateien/2009/august/14/25.png";
var image = new Image();
image.src = imgUrl ;
$(image).load(function() {
ctx.drawImage(image, 0, 0, canvas.width(), canvas.height());
draw();
});
function draw() {
ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
ctx.fillRect(0, 0, canvas.width(), canvas.height());
setTimeout(draw, 100);
}
});
The effect one may want to achieve is that, say an object is moving all over the canvas, and the already drawn positions get overdrawn only slightly so after-glow of after-fade effect. But this result is just fugly.
So is there any solution to this?
I know this is old but I don't think the previously accepted answer is correct. I think this is happening as a result of pixel values being truncated from float to byte. In Windows 7 running Chrome version 39.0.2171.95m, after running your fiddle for a while, the image is still visible but only lightly, and doesn't appear to be changing any more. If I take a screenshot I see the following pixel values on the image:
(246, 246, 246)
When you draw a rectangle over it with rgba of:
(255, 255, 255, 0.1)
and apply alpha blending using the default compositing mode of source-over, before converting to a byte you get:
(255 * 0.1 + 246 * 0.9) = 246.9
So you can see that, assuming the browser simply truncates the floating point value to a byte, it will write out a value of 246, and every time you repeat the drawing operation you'll always end up with the same value.
There is a big discussion on the issue at this blog post here.
As a workaround you could continually clear the canvas and redraw the image with a decreasing globalAlpha value. For example:
// Clear the canvas
ctx.globalAlpha = 1.0;
ctx.fillStyle = "rgb(255, 255, 255)";
ctx.fillRect(0,0,canvas.width(),canvas.height());
// Decrement the alpha and draw the image
alpha -= 0.1;
if (alpha < 0) alpha = 0;
ctx.globalAlpha = alpha;
console.log(alpha);
ctx.drawImage(image, 0, 0, 256, 256);
setTimeout(draw, 100);
Fiddle is here.
Since the rectangle is only 10% opaque, the result of drawing it over the image is a composite of 90% of the image and 10% white. Each time you draw it you lose 10% of the previous iteration of the image; the rectangle itself does not become more opaque. (To get that effect, you would need to position another object over the image and animate its opacity.) So after 10 iterations you still have (0.9^10) or about 35% of the original image. Note that rounding errors will probably set in after about 30 iterations.
The reason was perfectly stated before. It is not possible to get rid of it without clearing it and redrawing it like #Sam already said.
What you can you do to compensate it a bit is to set globalCompositeOperation.
There are various operations that help. From my tests I can say that hard-light works best for dark backgrounds and lighter work best for bright backgrounds. But this very depends on your scene.
An example making trails on "near" black
ctx.globalCompositeOperation = 'hard-light'
ctx.fillStyle = 'rgba(20,20,20,0.2)' // The closer to black the better
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = 'source-over' // reset to default value
The solution is to manipulate the pixel data with ctx.getImageData and ctx.putImageData.
Instead of using ctx.fillRect with a translucent fillStyle, set each pixel slightly to your background colour each frame. In my case it is black, which makes things simpler.
With this solution, your trails can be as long as you want, if float precision is taken into account.
function postProcess(){
const fadeAmount = 1-1/256;
const imageData = ctx.getImageData(0, 0, w, h);
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
const i = (x + y * w) * 4;
imageData.data[i] = Math.floor(imageData.data[i]*fadeAmount);
imageData.data[i + 1] = Math.floor(imageData.data[i + 1]*fadeAmount);
imageData.data[i + 2] = Math.floor(imageData.data[i + 2]*fadeAmount);
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const w = window.innerWidth;
const h = window.innerHeight;
canvas.width = w;
canvas.height = h;
const cs = createCs(50);
let frame = 0;
function init(){
ctx.strokeStyle = '#FFFFFF';
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, w, h)
loop();
}
function createCs(n){
const cs = [];
for(let i = 0; i < n; i++){
cs.push({
x: Math.random()*w,
y: Math.random()*h,
r: Math.random()*5+1
});
}
return cs;
}
function draw(frame){
//no longer need these:
//ctx.fillStyle = 'rgba(0,0,0,0.02)'
//ctx.fillRect(0, 0, w, h)
ctx.beginPath();
cs.forEach(({x,y,r}, i) => {
cs[i].x += 0.5;
if(cs[i].x > w) cs[i].x = -r;
ctx.moveTo(x+r+Math.cos((frame+i*4)/30)*r, y+Math.sin((frame+i*4)/30)*r);
ctx.arc(x+Math.cos((frame+i*4)/30)*r,y+Math.sin((frame+i*4)/30)*r,r,0,Math.PI*2);
});
ctx.closePath();
ctx.stroke();
//only fade every 4 frames
if(frame % 4 === 0) postProcess(0,0,w,h*0.5);
//fade every frame
postProcess(0,h*0.5,w,h*0.5);
}
//fades canvas to black
function postProcess(sx,sy,dw,dh){
sx = Math.round(sx);
sy = Math.round(sy);
dw = Math.round(dw);
dh = Math.round(dh);
const fadeAmount = 1-4/256;
const imageData = ctx.getImageData(sx, sy, dw, dh);
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
const i = (x + y * w) * 4;
imageData.data[i] = Math.floor(imageData.data[i]*fadeAmount);
imageData.data[i + 1] = Math.floor(imageData.data[i + 1]*fadeAmount);
imageData.data[i + 2] = Math.floor(imageData.data[i + 2]*fadeAmount);
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, sx, sy);
}
function loop(){
draw(frame);
frame ++;
requestAnimationFrame(loop);
}
init();
canvas {
width: 100%;
height: 100%;
}
<canvas id="canvas"/>
I have this awesome piece of code.
The idea, as you can imagine,is to draw a grid of rectangles. I want a big grid, let's say 100 X 100 or more.
However, when i run the awesome piece of code for the desired size (100X 100), my browser crashes.
How can i achieve that?
* please note: when i say 100X100 i mean the final number of rectangles (10k) not the size of the canvas.
thank u
function init() {
var cnv = get('cnv');
var ctx = cnv.getContext('2d');
var ancho = 12; // ancho means width
var alto = 12; // alto means height
ctx.fillStyle = randomRGB();
for (var i = 0; i < cnv.width; i+= ancho) {
for (var j = 0; j < cnv.height; j+= alto) {
//dibujar means to draw, rectangulo means rectangle
dibujarRectangulo(i+ 1, j+1, ancho, alto, ctx);
}
}
}
function dibujarRectangulo(x, y, ancho, alto, ctx) {
ctx.rect(x, y, ancho, alto);
ctx.fill();
ctx.closePath();
}
The dibujarRectanglo() function calls rect() function which adds a closed rectanglar subpath to the current path. Then calls fill() function to fill the current path. Then calls closePath() function to close the subpath, which does nothing since the subpath is already closed.
In other words, the first dibujarRectanglo() function call is painting a path that contains 1 rectangle subpath. The second call is painting a path that contains 2 rectangle subpaths. The third call is painting a path that contains 3 rectangle subpaths. And so on. If the loop calls dibujarRectanglo() function 10000 times then a total of 1+2+3+...+10000 = 50005000 (i.e. over 50 million) rectangle subpaths will be painted.
The dibujarRectangle() function should be starting a new path each time. For example...
function dibujarRectangulo(x, y, ancho, alto, ctx) {
ctx.beginPath();
ctx.rect(x, y, ancho, alto);
ctx.fill();
}
Then 10000 calls will only paint 10000 rectangle subpaths which is a lot faster that painting 50 million rectangle subpaths.
16,384 boxes on the wall
As I said in the comment its easy to draw a lot of boxes, it is not easy to have them all behave uniquely. Anyways using render to self to duplicate boxes exponential there are 128 * 128 boxes so that's 16K, one more iteration and it would be 64K boxes.
Its a cheat, I could have just drawn random pixels and called each pixel a box.
Using canvas you will get upto 4000 sprites per frame on a top end machine using FireFox with each sprite having a location, center point, rotation, x and y scale, and an alpha value. But that is the machine going flat out.
Using WebGL you can get much higher but the code complexity goes up.
I use a general rule of thumb, if a canva 2D project has more than 1000 sprites then it is in need of redesign.
var canvas = document.getElementById("can");
var ctx = canvas.getContext("2d");
/** CreateImage.js begin **/
var createImage = function (w, h) {
var image = document.createElement("canvas");
image.width = w;
image.height = h;
image.ctx = image.getContext("2d");
return image;
}
/** CreateImage.js end **/
/** FrameUpdate.js begin **/
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;
var ch = h / 2;
var boxSize = 10;
var boxSizeH = 5;
var timeDiv = 1.2;
var bBSize = boxSize * 128; // back buffer ssize
var buff = createImage(bBSize, bBSize);
var rec = createImage(boxSize, boxSize);
var drawRec = function (ctx, time) {
var size, x, y;
size = (Math.sin(time / 200) + 1) * boxSizeH;
ctx.fillStyle = "hsl(" + Math.floor((Math.sin(time / 500) + 1) * 180) + ",100%,50%)";
ctx.strokeStyle = "Black";
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.clearRect(0, 0, boxSize, boxSize);
x = Math.cos(time / 400);
y = Math.sin(time / 400);
ctx.setTransform(x, y, -y, x, boxSizeH, boxSizeH)
ctx.fillRect(-boxSizeH + size, -boxSizeH + size, boxSize - 2 * size, boxSize - 2 * size);
ctx.strokeRect(-boxSizeH + size, -boxSizeH + size, boxSize - 2 * size, boxSize - 2 * size);
}
function update(time) {
var fw, fh, px, py, i;
time /= 7;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, w, h);
drawRec(rec.ctx, time);
time /= timeDiv;
buff.ctx.clearRect(0, 0, bBSize, bBSize)
buff.ctx.drawImage(rec, 0, 0);
buff.ctx.drawImage(rec, boxSize, 0);
fw = boxSize + boxSize; // curent copy area width
fh = boxSize; // curent copy area height
px = 0; // current copy to x pos
py = boxSize; // current copy to y pos
buff.ctx.drawImage(buff, 0, 0, fw, fh, px, py, fw, fh); // make square
for (i = 0; i < 6; i++) {
drawRec(rec.ctx, time);
time /= timeDiv;
buff.ctx.drawImage(rec, 0, 0);
fh += fh; // double size across
px = fw;
py = 0;
buff.ctx.drawImage(buff, 0, 0, fw, fh, px, py, fw, fh); // make rec
drawRec(rec.ctx, time);
time /= timeDiv;
buff.ctx.drawImage(rec, 0, 0);
fw += fw; // double size down
px = 0;
py = fh;
buff.ctx.drawImage(buff, 0, 0, fw, fh, px, py, fw, fh);
}
// draw the boxes onto the canvas,
ctx.drawImage(buff, 0, 0, 1024, 1024);
requestAnimationFrame(update);
}
update();
.canv {
width:1024px;
height:1024px;
}
<canvas id="can" class = "canv" width=1024 height=1024></canvas>
I want to write a simple scrolling right to left starfield. I have printed out the stars randomly. Now, how do I target each star and randomly give it a speed (say 1-10) and begin moving it? I also need to put each star back on the right edge after it reaches the left edge.
Following is my code written so far:
<!DOCTYPE html>
<html>
<head>
<script>
function stars()
{
canvas = document.getElementById("can");
if(canvas.getContext)
{
ctx = canvas.getContext("2d");
ctx.fillStyle = "black";
ctx.rect (0, 0, 400, 400);
ctx.fill();
starfield();
}
}
//print random stars
function starfield()
{
for (i=0; i<10; i++)
{
var x = Math.floor(Math.random()*399);
var y = Math.floor(Math.random()*399);
var tempx = x;
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}
}
</script>
</head>
<body onload="stars()">
<h1>Stars</h1>
<canvas id="can" width="400" height="400"style="border:2px solid #000100" ></canvas>
</body >
</html>
Here's a quick demo on Codepen. After saving the stars in an array, I'm using requestAnimationFrame to run the drawing code and update the position on every frame.
function stars() {
canvas = document.getElementById("can");
console.log(canvas);
if (canvas.getContext) {
ctx = canvas.getContext("2d");
ctx.fillStyle = "black";
ctx.rect(0, 0, 400, 400);
ctx.fill();
starfield();
}
}
// Create random stars with random velocity.
var starList = []
function starfield() {
for (i = 0; i < 20; i++) {
var star = {
x: Math.floor(Math.random() * 399),
y: Math.floor(Math.random() * 399),
vx: Math.ceil(Math.random() * 10)
};
starList.push(star);
}
}
function run() {
// Register for the next frame
window.requestAnimationFrame(run);
// Reset the canvas
ctx.fillStyle = "black";
ctx.rect(0, 0, 400, 400);
ctx.fill();
// Update position and draw each star.
var star;
for(var i=0, j=starList.length; i<j; i++) {
star = starList[i];
star.x = (star.x - star.vx + 400) % 400;
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(star.x, star.y, 3, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
}
}
stars();
run();
Put your x,y coordinates in an array, and then make a function that draws the array.
var stars = [
{x:110, y:80},
{x:120, y:20},
{x:130, y:60},
{x:140, y:40}
]
Then make a function to alter the x,y coordinates (for example increment y=y+1) each time before using the draw function.
Bonus:
This array solution allows you to have each star move at its own speed, you could store a delta (say 1 upto 3) in that array, and do y=y+delta instead. This looks 3D.
You could even go further and have a seperate x and y delta, and have stars fly out from the middle, which is even more 3D!
Or even simpler/faster could be to have the render function accept an x,y offset. It could then even wrap around, so that what falls off the screen on one side comes back on the other. It looks like you are rotating in space.
I simple way to imitate star movement towards a point(like a center) is simply divide both X and Y by Z coordinate.
nx = x / z
ny = y / z
And simply decrease z value as you iterate. As z is big, your points will be around a point and as z decreases the result will be bigger and bigger which imitates "moving" of a stars.
Just providing a solution which uses jQuery because using it you can get the output with lesser lines of code compared to complete canvas solution.It uses two canvas divs to get the desired output:
Check this fiddle
Little updated code from the code posted in the question
<script>
function stars(){
canvas = document.getElementById("can1");
canvasCopy = document.getElementById("can2");
if(canvas.getContext){
ctx = canvas.getContext("2d");
ctx.fillStyle = "black";
ctx.rect (0, 0, 400, 400);
ctx.fill();
starfield();
var destCtx = canvasCopy.getContext('2d');
destCtx.drawImage(canvas, 0, 0);
}
}
//print random stars
function starfield(){
for (i=0;i<10;i++){
var x = Math.floor(Math.random()*399);
var y = Math.floor(Math.random()*399);
var tempx = x;
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}
}
</script>
<body onload="stars()">
<h1>Stars</h1>
<div id="starBlocks">
<canvas id="can1" width="400" height="400"style="border:2px solid #000100" ></canvas>
<canvas id="can2" width="400" height="400"style="border:2px solid #000100" ></canvas>
</div>
</body >
jQuery
function playStars()
{
$('#starBlocks').animate({
scrollLeft : 400
},10000,'linear',function(){
$('#starBlocks').scrollLeft(0);
playStars();
});
}
playStars();
CSS
#starBlocks{
white-space:nowrap;
font-size:0px;
width:400px;
overflow:hidden;
}
I stubled upon a weird problem. The following code results in making the image fade away because it's overdrawn by a semi-opaque rect over and over again.
But at least at the 10th iteration of draw(); the image should be completely overdrawn, because the rect should be fully opaque by then, right? But it actually never disappears completely.
This effect is worse on Chrome than it is on Firefox. But beware: bad screens may hide this faulty behaviour =)
I also made a demo on jsFiddle.
$(function () {
var canvas = $("#mycanvas"),
ctx = canvas[0].getContext("2d"),
imgUrl = "http://it-runde.de/dateien/2009/august/14/25.png";
var image = new Image();
image.src = imgUrl ;
$(image).load(function() {
ctx.drawImage(image, 0, 0, canvas.width(), canvas.height());
draw();
});
function draw() {
ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
ctx.fillRect(0, 0, canvas.width(), canvas.height());
setTimeout(draw, 100);
}
});
The effect one may want to achieve is that, say an object is moving all over the canvas, and the already drawn positions get overdrawn only slightly so after-glow of after-fade effect. But this result is just fugly.
So is there any solution to this?
I know this is old but I don't think the previously accepted answer is correct. I think this is happening as a result of pixel values being truncated from float to byte. In Windows 7 running Chrome version 39.0.2171.95m, after running your fiddle for a while, the image is still visible but only lightly, and doesn't appear to be changing any more. If I take a screenshot I see the following pixel values on the image:
(246, 246, 246)
When you draw a rectangle over it with rgba of:
(255, 255, 255, 0.1)
and apply alpha blending using the default compositing mode of source-over, before converting to a byte you get:
(255 * 0.1 + 246 * 0.9) = 246.9
So you can see that, assuming the browser simply truncates the floating point value to a byte, it will write out a value of 246, and every time you repeat the drawing operation you'll always end up with the same value.
There is a big discussion on the issue at this blog post here.
As a workaround you could continually clear the canvas and redraw the image with a decreasing globalAlpha value. For example:
// Clear the canvas
ctx.globalAlpha = 1.0;
ctx.fillStyle = "rgb(255, 255, 255)";
ctx.fillRect(0,0,canvas.width(),canvas.height());
// Decrement the alpha and draw the image
alpha -= 0.1;
if (alpha < 0) alpha = 0;
ctx.globalAlpha = alpha;
console.log(alpha);
ctx.drawImage(image, 0, 0, 256, 256);
setTimeout(draw, 100);
Fiddle is here.
Since the rectangle is only 10% opaque, the result of drawing it over the image is a composite of 90% of the image and 10% white. Each time you draw it you lose 10% of the previous iteration of the image; the rectangle itself does not become more opaque. (To get that effect, you would need to position another object over the image and animate its opacity.) So after 10 iterations you still have (0.9^10) or about 35% of the original image. Note that rounding errors will probably set in after about 30 iterations.
The reason was perfectly stated before. It is not possible to get rid of it without clearing it and redrawing it like #Sam already said.
What you can you do to compensate it a bit is to set globalCompositeOperation.
There are various operations that help. From my tests I can say that hard-light works best for dark backgrounds and lighter work best for bright backgrounds. But this very depends on your scene.
An example making trails on "near" black
ctx.globalCompositeOperation = 'hard-light'
ctx.fillStyle = 'rgba(20,20,20,0.2)' // The closer to black the better
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = 'source-over' // reset to default value
The solution is to manipulate the pixel data with ctx.getImageData and ctx.putImageData.
Instead of using ctx.fillRect with a translucent fillStyle, set each pixel slightly to your background colour each frame. In my case it is black, which makes things simpler.
With this solution, your trails can be as long as you want, if float precision is taken into account.
function postProcess(){
const fadeAmount = 1-1/256;
const imageData = ctx.getImageData(0, 0, w, h);
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
const i = (x + y * w) * 4;
imageData.data[i] = Math.floor(imageData.data[i]*fadeAmount);
imageData.data[i + 1] = Math.floor(imageData.data[i + 1]*fadeAmount);
imageData.data[i + 2] = Math.floor(imageData.data[i + 2]*fadeAmount);
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const w = window.innerWidth;
const h = window.innerHeight;
canvas.width = w;
canvas.height = h;
const cs = createCs(50);
let frame = 0;
function init(){
ctx.strokeStyle = '#FFFFFF';
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, w, h)
loop();
}
function createCs(n){
const cs = [];
for(let i = 0; i < n; i++){
cs.push({
x: Math.random()*w,
y: Math.random()*h,
r: Math.random()*5+1
});
}
return cs;
}
function draw(frame){
//no longer need these:
//ctx.fillStyle = 'rgba(0,0,0,0.02)'
//ctx.fillRect(0, 0, w, h)
ctx.beginPath();
cs.forEach(({x,y,r}, i) => {
cs[i].x += 0.5;
if(cs[i].x > w) cs[i].x = -r;
ctx.moveTo(x+r+Math.cos((frame+i*4)/30)*r, y+Math.sin((frame+i*4)/30)*r);
ctx.arc(x+Math.cos((frame+i*4)/30)*r,y+Math.sin((frame+i*4)/30)*r,r,0,Math.PI*2);
});
ctx.closePath();
ctx.stroke();
//only fade every 4 frames
if(frame % 4 === 0) postProcess(0,0,w,h*0.5);
//fade every frame
postProcess(0,h*0.5,w,h*0.5);
}
//fades canvas to black
function postProcess(sx,sy,dw,dh){
sx = Math.round(sx);
sy = Math.round(sy);
dw = Math.round(dw);
dh = Math.round(dh);
const fadeAmount = 1-4/256;
const imageData = ctx.getImageData(sx, sy, dw, dh);
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
const i = (x + y * w) * 4;
imageData.data[i] = Math.floor(imageData.data[i]*fadeAmount);
imageData.data[i + 1] = Math.floor(imageData.data[i + 1]*fadeAmount);
imageData.data[i + 2] = Math.floor(imageData.data[i + 2]*fadeAmount);
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, sx, sy);
}
function loop(){
draw(frame);
frame ++;
requestAnimationFrame(loop);
}
init();
canvas {
width: 100%;
height: 100%;
}
<canvas id="canvas"/>