为什么 canvas 有黑色背景?
Why does the canvas have a black background?
下面canvas为什么是黑色背景?因此,canvas 后面的文本不可见。我需要它是透明的以显示它后面的元素和主体的背景图像。
Stack Overflow 上有一个类似的问题,但针对该问题给出的解决方案对我来说并不适用。
"use strict";
let canvas, width, height, ctx;
let fireworks = [];
let particles = [];
function setup() {
canvas = document.getElementById("canvas");
setSize(canvas);
ctx = canvas.getContext("2d");
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
fireworks.push(new Firework(Math.random()*(width-200)+100));
window.addEventListener("resize",windowResized);
document.addEventListener("click",onClick);
}
setTimeout(setup,1);
function loop(){
ctx.globalAlpha = 0.1;
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
for(let i=0; i<fireworks.length; i++){
let done = fireworks[i].update();
fireworks[i].draw();
if(done) fireworks.splice(i, 1);
}
for(let i=0; i<particles.length; i++){
particles[i].update();
particles[i].draw();
if(particles[i].lifetime>80) particles.splice(i,1);
}
if(Math.random()<1/60) fireworks.push(new Firework(Math.random()*(width-200)+100));
}
setInterval(loop, 1/60);
//setInterval(loop, 100/60);
class Particle{
constructor(x, y, col){
this.x = x;
this.y = y;
this.col = col;
this.vel = randomVec(2);
this.lifetime = 0;
}
update(){
this.x += this.vel.x;
this.y += this.vel.y;
this.vel.y += 0.02;
this.vel.x *= 0.99;
this.vel.y *= 0.99;
this.lifetime++;
}
draw(){
ctx.globalAlpha = Math.max(1-this.lifetime/80, 0);
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
class Firework{
constructor(x){
this.x = x;
this.y = height;
this.isBlown = false;
this.col = randomCol();
}
update(){
this.y -= 3;
if(this.y < 350-Math.sqrt(Math.random()*500)*40){
this.isBlown = true;
for(let i=0; i<60; i++){
particles.push(new Particle(this.x, this.y, this.col))
}
}
return this.isBlown;
}
draw(){
ctx.globalAlpha = 1;
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
function randomCol(){
var letter = '0123456789ABCDEF';
var nums = [];
for(var i=0; i<3; i++){
nums[i] = Math.floor(Math.random()*256);
}
let brightest = 0;
for(var i=0; i<3; i++){
if(brightest<nums[i]) brightest = nums[i];
}
brightest /=255;
for(var i=0; i<3; i++){
nums[i] /= brightest;
}
let color = "#";
for(var i=0; i<3; i++){
color += letter[Math.floor(nums[i]/16)];
color += letter[Math.floor(nums[i]%16)];
}
return color;
}
function randomVec(max){
let dir = Math.random()*Math.PI*2;
let spd = Math.random()*max;
return{x: Math.cos(dir)*spd, y: Math.sin(dir)*spd};
}
function setSize(canv){
canv.style.width = (innerWidth) + "px";
canv.style.height = (innerHeight) + "px";
width = innerWidth;
height = innerHeight;
canv.width = innerWidth*window.devicePixelRatio;
canv.height = innerHeight*window.devicePixelRatio;
canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}
function onClick(e){
fireworks.push(new Firework(e.clientX));
}
function windowResized(){
setSize(canvas);
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
}
<canvas id="canvas"></canvas>
<h1>Hello</h1>
据我了解,您想为 canvas 设置白色背景?看看这个!
"use strict";
let canvas, width, height, ctx;
let fireworks = [];
let particles = [];
function setup() {
canvas = document.getElementById("canvas");
setSize(canvas);
ctx = canvas.getContext("2d");
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
fireworks.push(new Firework(Math.random()*(width-200)+100));
window.addEventListener("resize",windowResized);
document.addEventListener("click",onClick);
}
setTimeout(setup,1);
function loop(){
ctx.globalAlpha = 0.1;
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
for(let i=0; i<fireworks.length; i++){
let done = fireworks[i].update();
fireworks[i].draw();
if(done) fireworks.splice(i, 1);
}
for(let i=0; i<particles.length; i++){
particles[i].update();
particles[i].draw();
if(particles[i].lifetime>80) particles.splice(i,1);
}
if(Math.random()<1/60) fireworks.push(new Firework(Math.random()*(width-200)+100));
}
setInterval(loop, 1/60);
//setInterval(loop, 100/60);
class Particle{
constructor(x, y, col){
this.x = x;
this.y = y;
this.col = col;
this.vel = randomVec(2);
this.lifetime = 0;
}
update(){
this.x += this.vel.x;
this.y += this.vel.y;
this.vel.y += 0.02;
this.vel.x *= 0.99;
this.vel.y *= 0.99;
this.lifetime++;
}
draw(){
ctx.globalAlpha = Math.max(1-this.lifetime/80, 0);
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
class Firework{
constructor(x){
this.x = x;
this.y = height;
this.isBlown = false;
this.col = randomCol();
}
update(){
this.y -= 3;
if(this.y < 350-Math.sqrt(Math.random()*500)*40){
this.isBlown = true;
for(let i=0; i<60; i++){
particles.push(new Particle(this.x, this.y, this.col))
}
}
return this.isBlown;
}
draw(){
ctx.globalAlpha = 1;
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
function randomCol(){
var letter = '0123456789ABCDEF';
var nums = [];
for(var i=0; i<3; i++){
nums[i] = Math.floor(Math.random()*256);
}
let brightest = 0;
for(var i=0; i<3; i++){
if(brightest<nums[i]) brightest = nums[i];
}
brightest /=255;
for(var i=0; i<3; i++){
nums[i] /= brightest;
}
let color = "#";
for(var i=0; i<3; i++){
color += letter[Math.floor(nums[i]/16)];
color += letter[Math.floor(nums[i]%16)];
}
return color;
}
function randomVec(max){
let dir = Math.random()*Math.PI*2;
let spd = Math.random()*max;
return{x: Math.cos(dir)*spd, y: Math.sin(dir)*spd};
}
function setSize(canv){
canv.style.width = (innerWidth) + "px";
canv.style.height = (innerHeight) + "px";
width = innerWidth;
height = innerHeight;
canv.width = innerWidth*window.devicePixelRatio;
canv.height = innerHeight*window.devicePixelRatio;
canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}
function onClick(e){
fireworks.push(new Firework(e.clientX));
}
function windowResized(){
setSize(canvas);
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
}
<canvas id="canvas">
<h1>Hey Try this out!</h1>
</canvas>
将填充样式更改为:
ctx.fillStyle = "rgba(0,0,0,0)";
ctx.fillRect(0, 0, width, height);
我认为没有简单的方法可以解决您的问题。您需要将每个粒子存储到一个缓冲区数组中,其中包含有关其颜色、位置等的详细信息。
然后您需要遍历缓冲区并更新任何更改。
然后将缓冲区中的每个粒子重新绘制到新清除的屏幕上。
这一切都非常可行,但在浏览器上非常繁重,具体取决于它是 phone 还是桌面平板电脑等
最简单的解决方法是背景是纯色的,这正是您所拥有的。
否则你可能会很聪明,在 AV1 或 VP9 编解码器中使用循环视频,因为它们广泛支持透明度。
如果这些选项不适合,那么编码将是我能看到它工作的唯一方法,但任何超过 1000 个粒子的东西都可能导致脂肪挂起和滞后,而不是你期望的黄油般平滑的 FPS。
如果其他人知道这个问题的更好解决方案,那么我很想知道:D
globalCompositeOperation
要仅清除 alpha > 0 的像素,请使用 globalCompositeOperation 属性 并将其设置为 "destination-out"
。
这将允许您在不影响背景的情况下部分删除像素(留下痕迹)。
您需要将复合操作恢复到 "source-over"
。
对函数的改动loop
如下
function loop(){
ctx.globalAlpha = 0.1;
ctx.globalCompositeOperation = "destination-out"; // Add line
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "source-over"; // Add line
ctx.globalAlpha = 1;
//... rest of code
示例
代码段是经过上述更改的代码的副本。
同时添加细微的更改以阻止它因不正确的设置调用顺序而在 SO 代码段上崩溃。
"use strict";
let canvas, width, height, ctx;
let fireworks = [];
let particles = [];
function setup() {
canvas = document.getElementById("canvas");
setSize(canvas);
ctx = canvas.getContext("2d");
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
fireworks.push(new Firework(Math.random()*(width-200)+100));
window.addEventListener("resize",windowResized);
document.addEventListener("click",onClick);
setInterval(loop, 1/60);
}
setTimeout(setup,1);
var frameCount = 0;
function loop(){
if (frameCount++ % 2) {
ctx.globalAlpha = 0.2;
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
}
for(let i=0; i<fireworks.length; i++){
let done = fireworks[i].update();
fireworks[i].draw();
if(done) fireworks.splice(i, 1);
}
for(let i=0; i<particles.length; i++){
particles[i].update();
particles[i].draw();
if(particles[i].lifetime>80) particles.splice(i,1);
}
if(Math.random()<1/60) fireworks.push(new Firework(Math.random()*(width-200)+100));
}
class Particle{
constructor(x, y, col){
this.x = x;
this.y = y;
this.col = col;
this.vel = randomVec(2);
this.lifetime = 0;
}
update(){
this.x += this.vel.x;
this.y += this.vel.y;
this.vel.y += 0.02;
this.vel.x *= 0.99;
this.vel.y *= 0.99;
this.lifetime++;
}
draw(){
ctx.globalAlpha = Math.max(1-this.lifetime/80, 0);
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
class Firework{
constructor(x){
this.x = x;
this.y = height;
this.isBlown = false;
this.col = randomCol();
}
update(){
this.y -= 3;
if(this.y < 350-Math.sqrt(Math.random()*500)*40){
this.isBlown = true;
for(let i=0; i<60; i++){
particles.push(new Particle(this.x, this.y, this.col))
}
}
return this.isBlown;
}
draw(){
ctx.globalAlpha = 1;
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
function randomCol(){
var letter = '0123456789ABCDEF';
var nums = [];
for(var i=0; i<3; i++){
nums[i] = Math.floor(Math.random()*256);
}
let brightest = 0;
for(var i=0; i<3; i++){
if(brightest<nums[i]) brightest = nums[i];
}
brightest /=255;
for(var i=0; i<3; i++){
nums[i] /= brightest;
}
let color = "#";
for(var i=0; i<3; i++){
color += letter[Math.floor(nums[i]/16)];
color += letter[Math.floor(nums[i]%16)];
}
return color;
}
function randomVec(max){
let dir = Math.random()*Math.PI*2;
let spd = Math.random()*max;
return{x: Math.cos(dir)*spd, y: Math.sin(dir)*spd};
}
function setSize(canv){
canv.style.width = (innerWidth) + "px";
canv.style.height = (innerHeight) + "px";
width = innerWidth;
height = innerHeight;
canv.width = innerWidth*window.devicePixelRatio;
canv.height = innerHeight*window.devicePixelRatio;
canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}
function onClick(e){
fireworks.push(new Firework(e.clientX));
}
function windowResized(){
setSize(canvas);
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
}
body {
background: #333;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
p { color: white; }
h1 { color: white; }
code { background: #DEF; }
<canvas id="canvas"></canvas>
<h1>Trails</h1>
<p>Text under the canvas with trails using</p>
<code>ctx.globalCompositeOperation = "destination-out";</code>
更新以上代码片段
通过将透明 alpha 值加倍并将透明率减半,从示例中删除了残留像素。
一个解决方案是使用多层,这将负责整体衰落。
因此,在每一帧,您创建一个新图层,并降低所有先前图层的不透明度。当该不透明度低于某个阈值时,您将其从列表中删除。
然后您更改 draw()
方法以在这些层中注册绘图命令,而不是直接绘制上下文。该层将负责设置正确的 alpha(基于其自身的不透明度和要绘制的项目之一),并将绘制其框架中的所有项目。
基本上这些层仍然是当前帧,但我们不存储像素数据,只存储绘图命令。这在您的情况下非常容易,因为您的绘图设置数量相当有限(只有颜色、alpha、x 和 y),但其他人可能需要更复杂的 API.
无论如何,这是一个混乱的重写 proof-of-concept:
class Layer {
constructor(ctx) {
this.ctx = ctx;
this.commands = [];
this.alpha = 1;
}
update(delta) {
this.alpha *= 1 - (0.1 * delta);
return this.alpha < 0.1;
}
draw() {
this.commands.forEach(([color, alpha, x, y, width, height]) => {
this.ctx.fillStyle = color;
this.ctx.globalAlpha = this.alpha * alpha;
this.ctx.fillRect(x, y, width, height);
});
}
}
class Particle {
constructor(x, y, col) {
this.x = x;
this.y = y;
this.col = col;
this.vel = randomVec(2);
this.lifetime = 0;
}
update(delta) {
delta = 1;
this.x += this.vel.x;
this.y += this.vel.y;
this.vel.y += 0.02 * delta;
this.vel.x *= 1 - (delta * 0.01);
this.vel.y *= 1 - (delta * 0.01);
this.lifetime += delta;
return this.lifetime > 80;
}
draw() {
const color = this.col;
const alpha = Math.max(1 - this.lifetime / 80, 0);
const x = this.x;
const y = this.y;
const rad = 2;
currentLayer.commands.push([color, alpha, x, y, rad, rad]);
}
}
class Firework {
constructor(x) {
this.x = x;
this.y = height;
this.isBlown = false;
this.col = randomCol();
}
update(delta) {
this.y -= 3 * delta;
if (this.y < 350 - Math.sqrt(Math.random() * 500) * 40) {
this.isBlown = true;
for (let i = 0; i < 60; i++) {
particles.push(new Particle(this.x, this.y, this.col))
}
}
return this.isBlown;
}
draw() {
const color = this.col;
const alpha = 1;
const x = this.x;
const y = this.y;
const rad = 2;
currentLayer.commands.push([color, alpha, x, y, rad, rad]);
}
}
const canvas = document.querySelector("canvas");
let width, height, currentLayer;
const layers = [];
const fireworks = [];
const particles = [];
setSize(canvas);
const ctx = canvas.getContext("2d");
fireworks.push(new Firework(Math.random() * (width - 200) + 100));
window.addEventListener("resize", windowResized);
document.addEventListener("click", onClick);
let lastTime = document.timeline?.currentTime || performance.now();
requestAnimationFrame(loop);
function loop(time) {
// values were defined with a 60FPS base
// we now use rAF and thus need a delta time
// to honor the same expected speed on all devices
const delta = (time - lastTime) / (1000 / 60);
lastTime = time;
ctx.globalAlpha = 1;
ctx.clearRect(0, 0, width, height);
currentLayer = new Layer(ctx);
layers.push(currentLayer);
const ended = [];
fireworks.forEach((firework, index) => {
const done = firework.update(delta);
if (done) {
ended.push(index);
}
firework.draw();
});
// remove all ended, for last to first
ended.reverse().forEach((index) => {
fireworks.splice(index, 1);
});
ended.length = 0;
particles.forEach((particle, index) => {
const done = particle.update(delta);
particle.draw();
if (done) {
ended.push(index);
}
});
ended.reverse().forEach((index) => {
particles.splice(index, 1);
});
ended.length = 0;
layers.forEach((layer, index) => {
const done = layer.update(delta);
if (done) {
ended.push(index);
}
layer.draw();
});
ended.reverse().forEach((index) => {
layers.splice(index, 1);
});
if (Math.random() < 1 / 60) {
fireworks.push(new Firework(Math.random() * (width - 200) + 100));
}
requestAnimationFrame(loop);
}
function randomCol() {
var letter = '0123456789ABCDEF';
var nums = [];
for (var i = 0; i < 3; i++) {
nums[i] = Math.floor(Math.random() * 256);
}
let brightest = 0;
for (var i = 0; i < 3; i++) {
if (brightest < nums[i]) brightest = nums[i];
}
brightest /= 255;
for (var i = 0; i < 3; i++) {
nums[i] /= brightest;
}
let color = "#";
for (var i = 0; i < 3; i++) {
color += letter[Math.floor(nums[i] / 16)];
color += letter[Math.floor(nums[i] % 16)];
}
return color;
}
function randomVec(max) {
let dir = Math.random() * Math.PI * 2;
let spd = Math.random() * max;
return {
x: Math.cos(dir) * spd,
y: Math.sin(dir) * spd
};
}
function setSize(canv) {
canv.style.width = (innerWidth) + "px";
canv.style.height = (innerHeight) + "px";
width = innerWidth;
height = innerHeight;
canv.width = innerWidth * window.devicePixelRatio;
canv.height = innerHeight * window.devicePixelRatio;
canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}
function onClick(e) {
fireworks.push(new Firework(e.clientX));
}
function windowResized() {
setSize(canvas);
}
body{margin:0;/* checkered effect from */
--tint:rgba(255,255,255,0.9);background-image:linear-gradient(to right,var(--tint),var(--tint)),linear-gradient(to right,black 50%,white 50%),linear-gradient(to bottom,black 50%,white 50%);background-blend-mode:normal,difference,normal;background-size:2em 2em;
}
canvas{display: block}
<canvas></canvas>
下面canvas为什么是黑色背景?因此,canvas 后面的文本不可见。我需要它是透明的以显示它后面的元素和主体的背景图像。
Stack Overflow 上有一个类似的问题,但针对该问题给出的解决方案对我来说并不适用。
"use strict";
let canvas, width, height, ctx;
let fireworks = [];
let particles = [];
function setup() {
canvas = document.getElementById("canvas");
setSize(canvas);
ctx = canvas.getContext("2d");
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
fireworks.push(new Firework(Math.random()*(width-200)+100));
window.addEventListener("resize",windowResized);
document.addEventListener("click",onClick);
}
setTimeout(setup,1);
function loop(){
ctx.globalAlpha = 0.1;
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
for(let i=0; i<fireworks.length; i++){
let done = fireworks[i].update();
fireworks[i].draw();
if(done) fireworks.splice(i, 1);
}
for(let i=0; i<particles.length; i++){
particles[i].update();
particles[i].draw();
if(particles[i].lifetime>80) particles.splice(i,1);
}
if(Math.random()<1/60) fireworks.push(new Firework(Math.random()*(width-200)+100));
}
setInterval(loop, 1/60);
//setInterval(loop, 100/60);
class Particle{
constructor(x, y, col){
this.x = x;
this.y = y;
this.col = col;
this.vel = randomVec(2);
this.lifetime = 0;
}
update(){
this.x += this.vel.x;
this.y += this.vel.y;
this.vel.y += 0.02;
this.vel.x *= 0.99;
this.vel.y *= 0.99;
this.lifetime++;
}
draw(){
ctx.globalAlpha = Math.max(1-this.lifetime/80, 0);
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
class Firework{
constructor(x){
this.x = x;
this.y = height;
this.isBlown = false;
this.col = randomCol();
}
update(){
this.y -= 3;
if(this.y < 350-Math.sqrt(Math.random()*500)*40){
this.isBlown = true;
for(let i=0; i<60; i++){
particles.push(new Particle(this.x, this.y, this.col))
}
}
return this.isBlown;
}
draw(){
ctx.globalAlpha = 1;
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
function randomCol(){
var letter = '0123456789ABCDEF';
var nums = [];
for(var i=0; i<3; i++){
nums[i] = Math.floor(Math.random()*256);
}
let brightest = 0;
for(var i=0; i<3; i++){
if(brightest<nums[i]) brightest = nums[i];
}
brightest /=255;
for(var i=0; i<3; i++){
nums[i] /= brightest;
}
let color = "#";
for(var i=0; i<3; i++){
color += letter[Math.floor(nums[i]/16)];
color += letter[Math.floor(nums[i]%16)];
}
return color;
}
function randomVec(max){
let dir = Math.random()*Math.PI*2;
let spd = Math.random()*max;
return{x: Math.cos(dir)*spd, y: Math.sin(dir)*spd};
}
function setSize(canv){
canv.style.width = (innerWidth) + "px";
canv.style.height = (innerHeight) + "px";
width = innerWidth;
height = innerHeight;
canv.width = innerWidth*window.devicePixelRatio;
canv.height = innerHeight*window.devicePixelRatio;
canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}
function onClick(e){
fireworks.push(new Firework(e.clientX));
}
function windowResized(){
setSize(canvas);
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
}
<canvas id="canvas"></canvas>
<h1>Hello</h1>
据我了解,您想为 canvas 设置白色背景?看看这个!
"use strict";
let canvas, width, height, ctx;
let fireworks = [];
let particles = [];
function setup() {
canvas = document.getElementById("canvas");
setSize(canvas);
ctx = canvas.getContext("2d");
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
fireworks.push(new Firework(Math.random()*(width-200)+100));
window.addEventListener("resize",windowResized);
document.addEventListener("click",onClick);
}
setTimeout(setup,1);
function loop(){
ctx.globalAlpha = 0.1;
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
for(let i=0; i<fireworks.length; i++){
let done = fireworks[i].update();
fireworks[i].draw();
if(done) fireworks.splice(i, 1);
}
for(let i=0; i<particles.length; i++){
particles[i].update();
particles[i].draw();
if(particles[i].lifetime>80) particles.splice(i,1);
}
if(Math.random()<1/60) fireworks.push(new Firework(Math.random()*(width-200)+100));
}
setInterval(loop, 1/60);
//setInterval(loop, 100/60);
class Particle{
constructor(x, y, col){
this.x = x;
this.y = y;
this.col = col;
this.vel = randomVec(2);
this.lifetime = 0;
}
update(){
this.x += this.vel.x;
this.y += this.vel.y;
this.vel.y += 0.02;
this.vel.x *= 0.99;
this.vel.y *= 0.99;
this.lifetime++;
}
draw(){
ctx.globalAlpha = Math.max(1-this.lifetime/80, 0);
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
class Firework{
constructor(x){
this.x = x;
this.y = height;
this.isBlown = false;
this.col = randomCol();
}
update(){
this.y -= 3;
if(this.y < 350-Math.sqrt(Math.random()*500)*40){
this.isBlown = true;
for(let i=0; i<60; i++){
particles.push(new Particle(this.x, this.y, this.col))
}
}
return this.isBlown;
}
draw(){
ctx.globalAlpha = 1;
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
function randomCol(){
var letter = '0123456789ABCDEF';
var nums = [];
for(var i=0; i<3; i++){
nums[i] = Math.floor(Math.random()*256);
}
let brightest = 0;
for(var i=0; i<3; i++){
if(brightest<nums[i]) brightest = nums[i];
}
brightest /=255;
for(var i=0; i<3; i++){
nums[i] /= brightest;
}
let color = "#";
for(var i=0; i<3; i++){
color += letter[Math.floor(nums[i]/16)];
color += letter[Math.floor(nums[i]%16)];
}
return color;
}
function randomVec(max){
let dir = Math.random()*Math.PI*2;
let spd = Math.random()*max;
return{x: Math.cos(dir)*spd, y: Math.sin(dir)*spd};
}
function setSize(canv){
canv.style.width = (innerWidth) + "px";
canv.style.height = (innerHeight) + "px";
width = innerWidth;
height = innerHeight;
canv.width = innerWidth*window.devicePixelRatio;
canv.height = innerHeight*window.devicePixelRatio;
canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}
function onClick(e){
fireworks.push(new Firework(e.clientX));
}
function windowResized(){
setSize(canvas);
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
}
<canvas id="canvas">
<h1>Hey Try this out!</h1>
</canvas>
将填充样式更改为:
ctx.fillStyle = "rgba(0,0,0,0)";
ctx.fillRect(0, 0, width, height);
我认为没有简单的方法可以解决您的问题。您需要将每个粒子存储到一个缓冲区数组中,其中包含有关其颜色、位置等的详细信息。
然后您需要遍历缓冲区并更新任何更改。
然后将缓冲区中的每个粒子重新绘制到新清除的屏幕上。
这一切都非常可行,但在浏览器上非常繁重,具体取决于它是 phone 还是桌面平板电脑等
最简单的解决方法是背景是纯色的,这正是您所拥有的。
否则你可能会很聪明,在 AV1 或 VP9 编解码器中使用循环视频,因为它们广泛支持透明度。
如果这些选项不适合,那么编码将是我能看到它工作的唯一方法,但任何超过 1000 个粒子的东西都可能导致脂肪挂起和滞后,而不是你期望的黄油般平滑的 FPS。
如果其他人知道这个问题的更好解决方案,那么我很想知道:D
globalCompositeOperation
要仅清除 alpha > 0 的像素,请使用 globalCompositeOperation 属性 并将其设置为 "destination-out"
。
这将允许您在不影响背景的情况下部分删除像素(留下痕迹)。
您需要将复合操作恢复到 "source-over"
。
对函数的改动loop
如下
function loop(){
ctx.globalAlpha = 0.1;
ctx.globalCompositeOperation = "destination-out"; // Add line
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "source-over"; // Add line
ctx.globalAlpha = 1;
//... rest of code
示例
代码段是经过上述更改的代码的副本。
同时添加细微的更改以阻止它因不正确的设置调用顺序而在 SO 代码段上崩溃。
"use strict";
let canvas, width, height, ctx;
let fireworks = [];
let particles = [];
function setup() {
canvas = document.getElementById("canvas");
setSize(canvas);
ctx = canvas.getContext("2d");
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
fireworks.push(new Firework(Math.random()*(width-200)+100));
window.addEventListener("resize",windowResized);
document.addEventListener("click",onClick);
setInterval(loop, 1/60);
}
setTimeout(setup,1);
var frameCount = 0;
function loop(){
if (frameCount++ % 2) {
ctx.globalAlpha = 0.2;
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
}
for(let i=0; i<fireworks.length; i++){
let done = fireworks[i].update();
fireworks[i].draw();
if(done) fireworks.splice(i, 1);
}
for(let i=0; i<particles.length; i++){
particles[i].update();
particles[i].draw();
if(particles[i].lifetime>80) particles.splice(i,1);
}
if(Math.random()<1/60) fireworks.push(new Firework(Math.random()*(width-200)+100));
}
class Particle{
constructor(x, y, col){
this.x = x;
this.y = y;
this.col = col;
this.vel = randomVec(2);
this.lifetime = 0;
}
update(){
this.x += this.vel.x;
this.y += this.vel.y;
this.vel.y += 0.02;
this.vel.x *= 0.99;
this.vel.y *= 0.99;
this.lifetime++;
}
draw(){
ctx.globalAlpha = Math.max(1-this.lifetime/80, 0);
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
class Firework{
constructor(x){
this.x = x;
this.y = height;
this.isBlown = false;
this.col = randomCol();
}
update(){
this.y -= 3;
if(this.y < 350-Math.sqrt(Math.random()*500)*40){
this.isBlown = true;
for(let i=0; i<60; i++){
particles.push(new Particle(this.x, this.y, this.col))
}
}
return this.isBlown;
}
draw(){
ctx.globalAlpha = 1;
ctx.fillStyle = this.col;
ctx.fillRect(this.x, this.y, 2, 2);
}
}
function randomCol(){
var letter = '0123456789ABCDEF';
var nums = [];
for(var i=0; i<3; i++){
nums[i] = Math.floor(Math.random()*256);
}
let brightest = 0;
for(var i=0; i<3; i++){
if(brightest<nums[i]) brightest = nums[i];
}
brightest /=255;
for(var i=0; i<3; i++){
nums[i] /= brightest;
}
let color = "#";
for(var i=0; i<3; i++){
color += letter[Math.floor(nums[i]/16)];
color += letter[Math.floor(nums[i]%16)];
}
return color;
}
function randomVec(max){
let dir = Math.random()*Math.PI*2;
let spd = Math.random()*max;
return{x: Math.cos(dir)*spd, y: Math.sin(dir)*spd};
}
function setSize(canv){
canv.style.width = (innerWidth) + "px";
canv.style.height = (innerHeight) + "px";
width = innerWidth;
height = innerHeight;
canv.width = innerWidth*window.devicePixelRatio;
canv.height = innerHeight*window.devicePixelRatio;
canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}
function onClick(e){
fireworks.push(new Firework(e.clientX));
}
function windowResized(){
setSize(canvas);
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, width, height);
}
body {
background: #333;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
p { color: white; }
h1 { color: white; }
code { background: #DEF; }
<canvas id="canvas"></canvas>
<h1>Trails</h1>
<p>Text under the canvas with trails using</p>
<code>ctx.globalCompositeOperation = "destination-out";</code>
更新以上代码片段
通过将透明 alpha 值加倍并将透明率减半,从示例中删除了残留像素。
一个解决方案是使用多层,这将负责整体衰落。
因此,在每一帧,您创建一个新图层,并降低所有先前图层的不透明度。当该不透明度低于某个阈值时,您将其从列表中删除。
然后您更改 draw()
方法以在这些层中注册绘图命令,而不是直接绘制上下文。该层将负责设置正确的 alpha(基于其自身的不透明度和要绘制的项目之一),并将绘制其框架中的所有项目。
基本上这些层仍然是当前帧,但我们不存储像素数据,只存储绘图命令。这在您的情况下非常容易,因为您的绘图设置数量相当有限(只有颜色、alpha、x 和 y),但其他人可能需要更复杂的 API.
无论如何,这是一个混乱的重写 proof-of-concept:
class Layer {
constructor(ctx) {
this.ctx = ctx;
this.commands = [];
this.alpha = 1;
}
update(delta) {
this.alpha *= 1 - (0.1 * delta);
return this.alpha < 0.1;
}
draw() {
this.commands.forEach(([color, alpha, x, y, width, height]) => {
this.ctx.fillStyle = color;
this.ctx.globalAlpha = this.alpha * alpha;
this.ctx.fillRect(x, y, width, height);
});
}
}
class Particle {
constructor(x, y, col) {
this.x = x;
this.y = y;
this.col = col;
this.vel = randomVec(2);
this.lifetime = 0;
}
update(delta) {
delta = 1;
this.x += this.vel.x;
this.y += this.vel.y;
this.vel.y += 0.02 * delta;
this.vel.x *= 1 - (delta * 0.01);
this.vel.y *= 1 - (delta * 0.01);
this.lifetime += delta;
return this.lifetime > 80;
}
draw() {
const color = this.col;
const alpha = Math.max(1 - this.lifetime / 80, 0);
const x = this.x;
const y = this.y;
const rad = 2;
currentLayer.commands.push([color, alpha, x, y, rad, rad]);
}
}
class Firework {
constructor(x) {
this.x = x;
this.y = height;
this.isBlown = false;
this.col = randomCol();
}
update(delta) {
this.y -= 3 * delta;
if (this.y < 350 - Math.sqrt(Math.random() * 500) * 40) {
this.isBlown = true;
for (let i = 0; i < 60; i++) {
particles.push(new Particle(this.x, this.y, this.col))
}
}
return this.isBlown;
}
draw() {
const color = this.col;
const alpha = 1;
const x = this.x;
const y = this.y;
const rad = 2;
currentLayer.commands.push([color, alpha, x, y, rad, rad]);
}
}
const canvas = document.querySelector("canvas");
let width, height, currentLayer;
const layers = [];
const fireworks = [];
const particles = [];
setSize(canvas);
const ctx = canvas.getContext("2d");
fireworks.push(new Firework(Math.random() * (width - 200) + 100));
window.addEventListener("resize", windowResized);
document.addEventListener("click", onClick);
let lastTime = document.timeline?.currentTime || performance.now();
requestAnimationFrame(loop);
function loop(time) {
// values were defined with a 60FPS base
// we now use rAF and thus need a delta time
// to honor the same expected speed on all devices
const delta = (time - lastTime) / (1000 / 60);
lastTime = time;
ctx.globalAlpha = 1;
ctx.clearRect(0, 0, width, height);
currentLayer = new Layer(ctx);
layers.push(currentLayer);
const ended = [];
fireworks.forEach((firework, index) => {
const done = firework.update(delta);
if (done) {
ended.push(index);
}
firework.draw();
});
// remove all ended, for last to first
ended.reverse().forEach((index) => {
fireworks.splice(index, 1);
});
ended.length = 0;
particles.forEach((particle, index) => {
const done = particle.update(delta);
particle.draw();
if (done) {
ended.push(index);
}
});
ended.reverse().forEach((index) => {
particles.splice(index, 1);
});
ended.length = 0;
layers.forEach((layer, index) => {
const done = layer.update(delta);
if (done) {
ended.push(index);
}
layer.draw();
});
ended.reverse().forEach((index) => {
layers.splice(index, 1);
});
if (Math.random() < 1 / 60) {
fireworks.push(new Firework(Math.random() * (width - 200) + 100));
}
requestAnimationFrame(loop);
}
function randomCol() {
var letter = '0123456789ABCDEF';
var nums = [];
for (var i = 0; i < 3; i++) {
nums[i] = Math.floor(Math.random() * 256);
}
let brightest = 0;
for (var i = 0; i < 3; i++) {
if (brightest < nums[i]) brightest = nums[i];
}
brightest /= 255;
for (var i = 0; i < 3; i++) {
nums[i] /= brightest;
}
let color = "#";
for (var i = 0; i < 3; i++) {
color += letter[Math.floor(nums[i] / 16)];
color += letter[Math.floor(nums[i] % 16)];
}
return color;
}
function randomVec(max) {
let dir = Math.random() * Math.PI * 2;
let spd = Math.random() * max;
return {
x: Math.cos(dir) * spd,
y: Math.sin(dir) * spd
};
}
function setSize(canv) {
canv.style.width = (innerWidth) + "px";
canv.style.height = (innerHeight) + "px";
width = innerWidth;
height = innerHeight;
canv.width = innerWidth * window.devicePixelRatio;
canv.height = innerHeight * window.devicePixelRatio;
canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}
function onClick(e) {
fireworks.push(new Firework(e.clientX));
}
function windowResized() {
setSize(canvas);
}
body{margin:0;/* checkered effect from */
--tint:rgba(255,255,255,0.9);background-image:linear-gradient(to right,var(--tint),var(--tint)),linear-gradient(to right,black 50%,white 50%),linear-gradient(to bottom,black 50%,white 50%);background-blend-mode:normal,difference,normal;background-size:2em 2em;
}
canvas{display: block}
<canvas></canvas>