javascriptarraysevent-handlinglistenerrequestanimationframe

Array push() is losing elements added by the class listener


The code should draw shapes class CircleFade one after another continuously. Each shape should be drawn over 100 cycles, then a new shape should be generated and the process repeated in the chain. A class listener setOnCycleEndListener() is used to trigger the creation of new shapes with function createShapeWithListener().

To draw CircleFade and other shapes either class RenderStack used collecting rendered shapes in RenderStack.shapes array. But this array does not updated by the class listener (it seems like it just dropped). However, the same operation works correctly when triggered by a buttonGenerate click.

const buttonGenerate = document.getElementById('button-generate');
const buttonAnimate = document.getElementById('button-animate');
const buttonDo = document.getElementById('button-do');
const canvas = document.getElementById('myCanvas');

const ctx = canvas.getContext('2d');

function rndColor() {
  return `rgb(${255 * Math.random()}, ${255 * Math.random()}, ${255 * Math.random()})`;
};

// Shape to draw
class CircleFade {
  constructor(ctx) {
    this.ctx = ctx;
    this.isActive = true;
    this.cycle = 100; // draw only 100 times
    this.x = Math.floor(Math.random() * canvas.width);
    this.y = Math.floor(Math.random() * canvas.height);
    this.onCycleEnd = null; // Листенер на событие конца цикла
  }

  draw() {
    this.ctx.beginPath();
    this.ctx.arc(this.x, this.y, 10, 0, 2 * Math.PI);
    this.ctx.fillStyle = rndColor();
    this.ctx.fill();
    this.ctx.stroke();

    this.cycle--;
    if (this.cycle <= 0) {
      this.isActive = false;
      if (this.onCycleEnd) {
        this.onCycleEnd(); // Implementing listener
      }
    }
  }

  // Listener setup
  setOnCycleEndListener(callback) {
    this.onCycleEnd = callback;
  }
}

// Class to collect  and draw shapes
class RenderStack {
  constructor(ctx) {
    this.ctx = ctx;
    this.shapes = [];
    this.isPlaying = false;
  }

  add(shape) {
    this.shapes.push(shape);
  }

  start() {
    this.isPlaying = true;
    this.render(); // Начинаем рендеринг
  }

  stop() {
    this.isPlaying = false;
  }

  render() {
    this.ctx.clearRect(0, 0, canvas.width, canvas.height);
    // Update shape list and draw it
    this.shapes = this.shapes.filter((item) => {
      item.draw();
      return item.isActive;
    });

    // Stop the animation if the list i empty
    const length = this.shapes.length;
    if (length <= 0) {
      this.isPlaying = false;
      this.ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    console.log(`length: ${this.shapes.length}`);

    if (this.isPlaying) {
      requestAnimationFrame(() => this.render());
    }
  }
}

// Create new Shape and adding listener to it
function createShapeWithListener() {
  const shape = new CircleFade(ctx);
  // ---(b.)--- Adding shape by listener
  shape.setOnCycleEndListener(() => {
    console.log(`Cycle ended for shape at (${shape.x}, ${shape.y}), creating a new one`);
    const newShape = createShapeWithListener();
    renderStack.add(newShape);

    if (!renderStack.isPlaying) {
      renderStack.start();
    }
  });
  return shape;
}

const renderStack = new RenderStack(ctx);
// ---(a.)--- Adding shape by button click
buttonGenerate.addEventListener('click', () => {
  renderStack.add(createShapeWithListener());
  if (!renderStack.isPlaying) buttonAnimate.click();
});

buttonAnimate.addEventListener('click', () => {
  renderStack.isPlaying ? renderStack.stop() : renderStack.start();
  buttonAnimate.textContent = renderStack.isPlaying ? 'Stop' : 'Start';
});

// buttonDo.addEventListener('click', () => {
// });
body {
  font-family: Arial, sans-serif;
}

canvas {
  border: 1px solid black;
}

button {
  margin-top: 10px;
}
<canvas id="myCanvas" width="640" height="480"></canvas>
<div style="display: flex;  flex-direction: row; gap: 2px; ">
  <button id='button-generate'>Generate Figure</button>
  <button id="button-animate">Start</button>
  <!-- <button id="button-do">Do</button> -->
</div>

The target is modification shapes list during rendering. Desirable to do it with listener mechanism (or similar) to allow spontaneous changes in time of processing based on randomly generated scenarios.


Solution

  • Your filter code in render() breaks things because it replaces the this.shapes array that addEventListeners are using

    Instead, modify the existing array

    const buttonGenerate = document.getElementById('button-generate');
    const buttonAnimate = document.getElementById('button-animate');
    const buttonDo = document.getElementById('button-do');
    const canvas = document.getElementById('myCanvas');
    
    const ctx = canvas.getContext('2d');
    
    function rndColor() {
      return `rgb(${255 * Math.random()}, ${255 * Math.random()}, ${255 * Math.random()})`;
    };
    
    // Shape to draw
    class CircleFade {
      constructor(ctx) {
        this.ctx = ctx;
        this.isActive = true;
        this.cycle = 100; // draw only 100 times
        this.x = Math.floor(Math.random() * canvas.width);
        this.y = Math.floor(Math.random() * canvas.height);
        this.onCycleEnd = null; // Листенер на событие конца цикла
      }
    
      draw() {
        this.ctx.beginPath();
        this.ctx.arc(this.x, this.y, 10, 0, 2 * Math.PI);
        this.ctx.fillStyle = rndColor();
        this.ctx.fill();
        this.ctx.stroke();
    
        this.cycle--;
        if (this.cycle <= 0) {
          this.isActive = false;
          if (this.onCycleEnd) {
            this.onCycleEnd(); // Implementing listener
          }
        }
      }
    
      // Listener setup
      setOnCycleEndListener(callback) {
        this.onCycleEnd = callback;
      }
    }
    
    // Class to collect  and draw shapes
    class RenderStack {
      constructor(ctx) {
        this.ctx = ctx;
        this.shapes = [];
        this.isPlaying = false;
      }
    
      add(shape) {
        this.shapes.push(shape);
      }
    
      start() {
        this.isPlaying = true;
        this.render(); // Начинаем рендеринг
      }
    
      stop() {
        this.isPlaying = false;
      }
    
      render() {
        this.ctx.clearRect(0, 0, canvas.width, canvas.height);
        // Update shape list and draw it
            // EDIT This code breaks things because it replaces the this.shapes array that addEventListeners are using
            // this.shapes = this.shapes.filter((item) => {
            //   item.draw();
            //   return item.isActive;
            // });
            // EDIT Modify the existing array, must work from end to beginning
            for( let next = this.shapes.length - 1; next >= 0; --next )  {
              // First draw it
              this.shapes[next].draw( )
              // Then remove it if it's finished it's itterations
              if( !this.shapes[next].isActive ) this.shapes.splice(next, 1)
            }
    
        // Stop the animation if the list i empty
        const length = this.shapes.length;
        if (length <= 0) {
          this.isPlaying = false;
          this.ctx.clearRect(0, 0, canvas.width, canvas.height);
        }
    
        if (this.isPlaying) {
          requestAnimationFrame(() => this.render());
        }
      }
    }
    
    // Create new Shape and adding listener to it
    function createShapeWithListener() {
      const shape = new CircleFade(ctx);
      // ---(b.)--- Adding shape by listener
      shape.setOnCycleEndListener(() => {
    //        console.log(`Cycle ended for shape at (${shape.x}, ${shape.y}), creating a new one`);
        const newShape = createShapeWithListener();
        renderStack.add(newShape);
    
        if (!renderStack.isPlaying) {
          renderStack.start();
        }
      });
      return shape;
    }
    
    const renderStack = new RenderStack(ctx);
    // ---(a.)--- Adding shape by button click
    buttonGenerate.addEventListener('click', () => {
      renderStack.add(createShapeWithListener());
      if (!renderStack.isPlaying) buttonAnimate.click();
    });
    
    buttonAnimate.addEventListener('click', () => {
      renderStack.isPlaying ? renderStack.stop() : renderStack.start();
      buttonAnimate.textContent = renderStack.isPlaying ? 'Stop' : 'Start';
    });
    
    // buttonDo.addEventListener('click', () => {
    // });
    body {
      font-family: Arial, sans-serif;
    }
    
    canvas {
      border: 1px solid black;
    }
    
    button {
      margin-top: 10px;
    }
    <div style="display: flex;  flex-direction: row; gap: 2px; ">
      <button id='button-generate'>Generate Figure</button>
      <button id="button-animate">Start</button>
    </div>
    <canvas id="myCanvas" width="440" height="180"></canvas>