为什么 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>