Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                
              
            
!

CSS

              
                html, body{
    margin:0;
    padding:0;
    background-color:#ffffff;
}

canvas{
    display:block;
    position:absolute;
    top:0;
    left:0;
}
              
            
!

JS

              
                /**
 * Raindrop fragment shader, being used by PIXI.js in the EffectCanvas object
 * {{uniforms: {time: {type: string, value: number}, iResolution: {type: string, value: [*]}}, fragment: string}}
 */
const shaderData = {
  uniforms: {
    iResolution: {
      type: 'v2',
      value: [
        window.innerWidth,
        window.innerHeight,
      ],
    },
    vTextureSize: {
      type: 'v2',
      value: [
        0,
        0,
      ],
    },
    uTextureForeground: {
      type: 'sampler2D',
      value: null,
    },
    uTextureBackground: {
      type: 'sampler2D',
      value: null,
    },
    uTextureDropShine: {
      type: 'sampler2D',
      value: null,
    },
  },

  fragment: `
        precision mediump float;
    
        //Textures
        uniform sampler2D uTextureForeground;
        uniform sampler2D uTextureBackground;
        uniform sampler2D uTextureDropShine;
        
        //Canvas image data
        uniform sampler2D uSampler;
    
        //The resolution and coordinates of the current pixel
        uniform vec2 iResolution;
        uniform vec2 vTextureSize;
        varying vec2 vTextureCoord;
        
        //Function to get the vec2 value of the current coordinate
        vec2 texCoord(){
            return vec2(gl_FragCoord.x, iResolution.y - gl_FragCoord.y) / iResolution;
        }

        //Scales the bg up and proportionally to fill the container
        vec2 scaledTextureCoordinate(){
            float ratioCanvas = iResolution.x / iResolution.y;
            float ratioImage = vTextureSize.x / vTextureSize.y;
            
            vec2 scale = vec2(1, 1);
            vec2 offset = vec2(0, 0);
            float ratioDelta = ratioCanvas - ratioImage;

            if(ratioDelta >= 0.0){
                scale.y = (1.0 + ratioDelta);
                offset.y = ratioDelta / 2.0;
            }else{
                scale.x = (1.0 - ratioDelta);
                offset.x = -(ratioDelta / 2.0);
            }

            return (texCoord() + offset) / scale;
        }
        
        //Alpha-blends two colors
        vec4 blend(vec4 bg, vec4 fg){
            vec3 bgm = bg.rgb * bg.a;
            vec3 fgm = fg.rgb * fg.a;
            float ia = 1.0 - fg.a;
            float a = (fg.a + bg.a * ia);
            
            vec3 rgb;
            
            if(a != 0.0){
                rgb = (fgm + bgm * ia) / a;
            }else{
                rgb = vec3(0.0,0.0,0.0);
            }
            
            return vec4(rgb,a);
        }
        
        vec2 pixel(){
            return vec2(1.0, 1.0) / iResolution;
        }
        
        //Get color from fg
        vec4 fgColor(){
            return texture2D(uSampler, vTextureCoord);
        }
                
        void main(){
            vec4 bg = texture2D(uTextureBackground, scaledTextureCoordinate());
            vec4 cur = fgColor();

            float d = cur.b; // "thickness"
            float x = cur.g;
            float y = cur.r;
            float a = smoothstep(0.65, 0.7, cur.a);
            
            vec4 smoothstepped = vec4(y, x, d, a);

            vec2 refraction = (vec2(x, y) - 0.5) * 2.0;
            vec2 refractionPos = scaledTextureCoordinate() + (pixel() * refraction * (256.0 + (d * 512.0)));
            vec4 tex = texture2D(uTextureForeground, refractionPos);
            
            float maxShine = 390.0;
            float minShine = maxShine * 0.18;
            vec2 shinePos = vec2(0.5, 0.5) + ((1.0 / 512.0) * refraction) * -(minShine + ((maxShine-minShine) * d));
            vec4 shine = texture2D(uTextureDropShine, shinePos);
            tex = blend(tex,shine);
            
            vec4 fg = vec4(tex.rgb, a);
            gl_FragColor = blend(bg, fg);
        }
	`,
};

/**
 * Application Class
 * Bootstraps the entire application and initializes all objects
 */
class Application {
  /**
   * Application constructor
   */
  constructor() {
    this.width = window.innerWidth;
    this.height = window.innerHeight;

    // Define the assets that PIXI needs to preload to use later in the application
    this.loader = PIXI.loader
      .add('https://stefanweck.nl/codepen/alpha.png')
      .add('https://stefanweck.nl/codepen/shine.png')
      .add('https://stefanweck.nl/codepen/background.jpg')
      .add('https://stefanweck.nl/codepen/foreground.jpg')
      .load(() => this.initialize());
  }

  /**
   * Initialize is ran when the image loader is done loading all resources
   * @return void
   */
  initialize() {
    // Create the Stats object and append it to the DOM
    this.stats = new Stats();
    this.stats.domElement.style.position = 'absolute';
    this.stats.domElement.style.left = '0px';
    this.stats.domElement.style.top = '0px';
    this.stats.domElement.style.zIndex = '9000';
    document.body.appendChild(this.stats.domElement);

    // Create a new instance of the EffectCanvas which is going to produce all of the visuals
    this.effectCanvas = new EffectCanvas(this.width, this.height, this.loader);

    // Resize listener for the canvas to fill browser window dynamically
    window.addEventListener('resize', () => this.resizeCanvas(), false);

    // Start the initial loop function for the first time
    this.loop();
  }

  /**
   * Simple resize function. Reinitializing everything on the canvas while changing the width/height
   * @return {void}
   */
  resizeCanvas() {
    this.width = window.innerWidth;
    this.height = window.innerHeight;

    this.effectCanvas.resize(this.width, this.height);
  }

  /**
   * Update and render the application at least 60 times a second
   * @return {void}
   */
  loop() {
    window.requestAnimationFrame(() => this.loop());

    this.stats.begin();

    this.effectCanvas.update(this.width, this.height);
    this.effectCanvas.render();

    this.stats.end();
  }
}

/**
 * EffectCanvas Class
 */
class EffectCanvas {
  /**
   * EffectCanvas constructor
   */
  constructor(width, height, loader) {
    // Create and configure the renderer
    this.renderer = new PIXI.autoDetectRenderer(width, height, {
      antialias: false,
      transparent: false,
    });
    this.renderer.autoResize = true;
    document.body.appendChild(this.renderer.view);

    // Create a container object called the `stage`
    this.stage = new PIXI.Container();

    // Create a graphics object that is as big as the scene of the users window
    // Else the shader won't fill the entire screen
    this.background = new PIXI.Graphics();
    this.background.fillAlphanumber = 0;
    this.background.beginFill('0xffffff');
    this.background.drawRect(0, 0, width, height);
    this.background.endFill();
    this.background.alpha = 0;
    this.stage.addChild(this.background);

    // Create the DropletManager and pass it the stage so it can insert the droplet containers into it
    this.dropletManager = new DropletManager(this.stage, loader);

    // Send information about the textures and the size of the background texture through the uniforms to the shader
    shaderData.uniforms.uTextureDropShine.value = loader.resources['https://stefanweck.nl/codepen/shine.png'].texture;
    shaderData.uniforms.uTextureBackground.value = loader.resources['https://stefanweck.nl/codepen/background.jpg'].texture;
    shaderData.uniforms.uTextureForeground.value = loader.resources['https://stefanweck.nl/codepen/foreground.jpg'].texture;
    shaderData.uniforms.vTextureSize.value = [
      loader.resources['https://stefanweck.nl/codepen/background.jpg'].texture.width,
      loader.resources['https://stefanweck.nl/codepen/background.jpg'].texture.height,
    ];

    // Create our Pixi filter using our custom shader code
    this.dropletShader = new PIXI.Filter('', shaderData.fragment, shaderData.uniforms);

    // Apply it to our object
    this.stage.filters = [this.dropletShader];
  }

  /**
   * Simple resize function which redraws our graphics object that should fill the entire screen
   * @return {void}
   */
  resize(width, height) {
    this.renderer.resize(width, height);

    this.background.clear();
    this.background.beginFill('0xffffff');
    this.background.drawRect(0, 0, width, height);
    this.background.endFill();
  }

  /**
   * Updates the application and every child of the application
   * @return {void}
   */
  update(width, height) {
    this.updateShader(width, height);
    this.dropletManager.update(width, height);
  }

  /**
   * Updates the uniform values in the shader
   * @return {void}
   */
  updateShader(width, height) {
    this.dropletShader.uniforms.iResolution = [
      width,
      height,
    ];
  }

  /**
   * Renders the application and every child of the application
   * @return {void}
   */
  render() {
    this.renderer.render(this.stage);
  }
}

/**
 * DropletManager class
 */
class DropletManager {
  /**
   * EffectCanvas constructor
   */
  constructor(stage, loader) {
    let smallDropletAmount = 9000;
    let largeDropletAmount = 200;

    //Quick implementation to make sure there aren't out of this world thunderstorms on mobile
    if(stage.width < 700){
      smallDropletAmount = 3000;
      largeDropletAmount = 150;
    }

    this.options = {
      spawnRate: {
        small: 0.6,
        large: 0.05,
      },
      spawnsPerFrame: {
        small: 200,
        large: 5,
      },
      spawnMass: {
        small: {
          min: 1,
          max: 2,
        },
        large: {
          min: 7,
          max: 10,
        },
      },
      poolDroplets: {
        small: {
          min: smallDropletAmount - 500,
          max: smallDropletAmount,
        },
        large: {
          min: largeDropletAmount - 100,
          max: largeDropletAmount,
        },
      },
      maximumMassGravity: 17,
      maximumMass: 21,
      dropletGrowSpeed: 1,
      dropletShrinkSpeed: 2,
      dropletContainerSize: 100,
    };

    // Define a position matrix so we can calculate all the edges of a droplet in a single loop
    this.positionMatrix = [
      [-1, -1],
      [1, -1],
      [-1, 1],
      [1, 1],
    ];

    this.smallDroplets = [];
    this.largeDroplets = [];

    this.dropletSmallTexture = loader.resources['https://stefanweck.nl/codepen/alpha.png'].texture;
    this.dropletLargeTexture = loader.resources['https://stefanweck.nl/codepen/alpha.png'].texture;

    // Create a container for all the droplets
    this.smallDropletContainer = new DropletPool(Droplet, this.dropletSmallTexture, this.options.poolDroplets.small.min, this.options.poolDroplets.small.max);
    this.largeDropletContainer = new DropletPool(LargeDroplet, this.dropletLargeTexture, this.options.poolDroplets.large.min, this.options.poolDroplets.large.max);

    stage.addChild(this.largeDropletContainer);
    stage.addChild(this.smallDropletContainer);
  }

  /**
   * Updates the application and every child of the application
   * @return {void}
   */
  update(width, height) {
    DropletManager.removeLargeOffscreenDroplets(width, height, this.largeDroplets, this.largeDropletContainer);

    // Trigger the spawn function for a small droplet as much times as is configured in the options
    for (let i = 0; i < this.options.spawnsPerFrame.small; i++) {
      this.spawnNewSmallDroplet(width, height);
    }

    // Trigger the spawn function for a large droplet as much times as is configured in the options
    for (let i = 0; i < this.options.spawnsPerFrame.large; i++) {
      this.spawnNewLargeDroplet(width, height);
    }

    // Check if we need to do anything with a large Droplet
    // We don't process small droplets because they are 'dumb' objects that don't move after they've spawned
    this.checkLargeDropletLogic();
  }

  /**
   * Checks whether a big droplet hits a smaller droplet, if so, it grows by half of the smaller droplets size
   * @return {void}
   */
  checkLargeDropletLogic() {
    // Store the length of the array so the for loop doesn't have to do that every run
    const largeDropletsLength = this.largeDroplets.length;

    for (let i = largeDropletsLength - 1; i >= 0; i--) {
      this.updateLargeDropletSize(this.largeDroplets[i]);
      this.checkDropletMovement(this.largeDroplets[i]);
      this.checkLargeToSmallDropletCollision(this.largeDroplets[i]);
      this.checkLargeToLargeDropletCollision(this.largeDroplets[i]);
      this.removeLargeDroplets(i);
    }
  }

  /**
   * Function that checks if a single large Droplet should be removed
   * @param i - The current droplet that we are processing
   */
  removeLargeDroplets(i) {
    if (this.largeDroplets[i].mass === 0 && this.largeDroplets[i].toBeRemoved === true) {
      this.largeDropletContainer.destroy(this.largeDroplets[i]);
      this.largeDroplets.splice(i, 1);
    }
  }

  /**
   * Function that updates the size of a single large Droplet
   * @param droplet
   */
  updateLargeDropletSize(droplet) {
    // If a droplet needs to be removed, we have to shrink it down to 0
    if (droplet.toBeRemoved === true) {
      this.shrinkDropletSize(droplet);
    } else {
      this.growDropletSize(droplet);
    }

    // Update the width and height of the droplet based on the new mass of the droplet
    droplet.width = droplet.mass * 6;
    droplet.height = droplet.mass * 7;
  }

  /**
   * Shrink a droplet based on the configured shrink speed. If it will be too small, we set the mass to 0
   * @param {LargeDroplet} droplet
   */
  shrinkDropletSize(droplet) {
    if (droplet.mass - this.options.dropletShrinkSpeed <= 0) {
      droplet.mass = 0;
    } else {
      droplet.mass -= this.options.dropletShrinkSpeed;
    }
  }

  /**
   * Grow a droplet based on the targetMass he has
   * @param {LargeDroplet} droplet
   */
  growDropletSize(droplet) {
    // If a droplet has already reached its target mass, exit here
    if (droplet.mass === droplet.targetMass) {
      return;
    }

    // Check if we can grow the droplet based on the configured grow speed
    if (droplet.mass + this.options.dropletGrowSpeed >= droplet.targetMass) {
      droplet.mass = droplet.targetMass;
    } else {
      droplet.mass += this.options.dropletGrowSpeed;
    }
  }

  /**
   * Check whether a large droplet should be moving or not
   * @param {LargeDroplet} droplet
   * @return {void}
   */
  checkDropletMovement(droplet) {
    // If the droplet is going to be removed at the end of this loop, don't bother checking it
    if (droplet.toBeRemoved === true) {
      return;
    }

    // Check if the droplets mass is high enough to be moving, and if the droplet is not moving yet
    if (droplet.mass < this.options.maximumMassGravity && droplet.dropletVelocity.y === 0 && droplet.dropletVelocity.x === 0) {
      // There's a slight chance that the droplet starts moving
      if (Math.random() < 0.01) {
        droplet.dropletVelocity.y = Utils.getRandomInt(0.5, 3);
      }
    } else if (droplet.mass < this.options.maximumMassGravity && droplet.dropletVelocity.y !== 0) {
      // There's a slight chance that the droplet shifts to the left or the right, just like real droplets attach to droplets near them
      if (Math.random() < 0.1) {
        droplet.x += Utils.getRandomInt(-10, 10) / 10;
      }

      // There's a slight chance that the droplet stops moving
      if (Math.random() < 0.1) {
        droplet.dropletVelocity.y = 0;
      }
    } else if (droplet.mass >= this.options.maximumMassGravity && droplet.dropletVelocity.y < 10) {
      // The droplet is falling because it is too heavy, its speed and direction are now set
      droplet.dropletVelocity.y = Utils.getRandomInt(10, 20);
      droplet.dropletVelocity.x = Utils.getRandomInt(-10, 10) / 10;
    }

    // Increase the x and y positions of the droplet based on its velocity
    droplet.y += droplet.dropletVelocity.y;
    droplet.x += droplet.dropletVelocity.x;
  }

  /**
   * Checks in which small droplet arrays the large droplet is positioned
   * @param {Droplet} droplet
   */
  getDropletPresenceArray(droplet) {
    // Define a set of array indexes through which we hava to search for collision
    const arrayIndexes = [];
    const length = this.positionMatrix.length;

    // Loop through each positionMatrix to calculate the position of every edge of a droplet
    for (let i = 0; i < length; i++) {
      const edgePosition = {
        x: Math.floor((droplet.x + ((droplet.width / 7) * this.positionMatrix[i][0])) / this.options.dropletContainerSize),
        y: Math.floor((droplet.y + ((droplet.height / 7) * this.positionMatrix[i][1])) / this.options.dropletContainerSize),
      };

      // Always push the first position in the arrayIndexes array, we use that value to compare the other edges to
      if (i === 0) {
        arrayIndexes.push(edgePosition);
        continue;
      }

      // If the current position differs from the first position, store the new value because that means that this is also an array we need to check for collision
      if (arrayIndexes[0].x !== edgePosition.x || arrayIndexes[0].y !== edgePosition.y) {
        arrayIndexes.push(edgePosition);
      }
    }

    return arrayIndexes;
  }

  /**
   * Check the collision between one large Droplet and all the other Droplets
   * @param droplet
   */
  checkLargeToLargeDropletCollision(droplet) {
    if (droplet.toBeRemoved === true) {
      return;
    }

    // Store the length of the droplets array so we have that valua cached in the for loop
    const length = this.largeDroplets.length;

    for (let i = length - 1; i >= 0; i--) {
      // Don't bother checking this droplet against itself
      if (droplet.x === this.largeDroplets[i].x && droplet.y === this.largeDroplets[i].y) {
        continue;
      }

      // Calculate the difference in position for the horizontal and the vertical axis
      const dx = droplet.x - this.largeDroplets[i].x;
      const dy = droplet.y - this.largeDroplets[i].y;

      // Calculate the distance between the current droplet and the current other droplet
      const distance = Math.sqrt((dx * dx) + (dy * dy));

      // If the distance between the droplets is close enough, the droplet colliding increases in size
      if (distance <= (droplet.width / 7) + (this.largeDroplets[i].width / 7)) {
        if (droplet.mass + this.largeDroplets[i].mass <= this.options.maximumMass) {
          droplet.targetMass = droplet.mass + this.largeDroplets[i].mass;
        } else {
          droplet.targetMass = this.options.maximumMass;
        }

        // The other droplet should be removed at the end of this loop
        this.largeDroplets[i].toBeRemoved = true;
      }
    }
  }

  /**
   * Checks whether a big droplet hits a smaller droplet, if so, it grows by half of the smaller droplets size
   * @param {LargeDroplet} droplet
   * @return {void}
   */
  checkLargeToSmallDropletCollision(droplet) {
    if (droplet.toBeRemoved === true) {
      return;
    }

    // Define a set of array indexes through which we have to search for collision
    const arrayIndexes = this.getDropletPresenceArray(droplet);

    for (let i = 0; i < arrayIndexes.length; i++) {
      // If the small droplet doesn't exist anymore, we can continue to the next value in the loop
      if (typeof this.smallDroplets[arrayIndexes[i].x] === 'undefined' || typeof this.smallDroplets[arrayIndexes[i].x][arrayIndexes[i].y] === 'undefined') {
        continue;
      }

      // Store the length of the array so the for loop doesn't have to do that every run
      const smallDropletsLength = this.smallDroplets[arrayIndexes[i].x][arrayIndexes[i].y].length;

      for (let c = smallDropletsLength - 1; c >= 0; c--) {
        // Calculate the difference in position for the horizontal and the vertical axis
        const dx = droplet.x - this.smallDroplets[arrayIndexes[i].x][arrayIndexes[i].y][c].x;
        const dy = droplet.y - this.smallDroplets[arrayIndexes[i].x][arrayIndexes[i].y][c].y;

        // Calculate the distance between the current droplet and the current other droplet
        const distance = Math.sqrt((dx * dx) + (dy * dy));

        // If the distance is small enough we can increase the size of the large droplet and remove the small droplet
        if (distance <= (droplet.width / 7) + (this.smallDroplets[arrayIndexes[i].x][arrayIndexes[i].y][c].width / 7)) {
          if (droplet.mass + (this.smallDroplets[arrayIndexes[i].x][arrayIndexes[i].y][c].mass / 3) <= this.options.maximumMass) {
            droplet.targetMass = droplet.mass + (this.smallDroplets[arrayIndexes[i].x][arrayIndexes[i].y][c].mass / 3);
          }

          // Remove the small droplet and put it back in the object pool
          this.smallDropletContainer.destroy(this.smallDroplets[arrayIndexes[i].x][arrayIndexes[i].y][c]);
          this.smallDroplets[arrayIndexes[i].x][arrayIndexes[i].y].splice(c, 1);
        }
      }
    }
  }

  /**
   * Spawns a new small droplet on the screen based on the spawn chance
   * @param {number} width
   * @param {number} height
   * @return {void}
   */
  spawnNewSmallDroplet(width, height) {
    // If our random value doesn't match the given spawn rate, we don't spawn a droplet
    if (Math.random() > this.options.spawnRate.small) {
      return;
    }

    // Get a new droplet object from the pool
    const droplet = this.smallDropletContainer.get();

    // If the pool decided that we can't add more droplets, exit here
    if (droplet === null) {
      return;
    }

    const position = {
      x: Utils.getRandomInt(0, width),
      y: Utils.getRandomInt(0, height),
    };
    const mass = Utils.getRandomInt(this.options.spawnMass.small.min, this.options.spawnMass.small.max);
    const arrayIndex = {
      x: Math.floor(position.x / this.options.dropletContainerSize),
      y: Math.floor(position.y / this.options.dropletContainerSize),
    };

    // Make sure the droplet updates with a new position and radius
    droplet.x = position.x;
    droplet.y = position.y;
    droplet.mass = mass;
    droplet.width = droplet.mass * 8;
    droplet.height = droplet.mass * 8;

    if (typeof this.smallDroplets[arrayIndex.x] === 'undefined') {
      this.smallDroplets[arrayIndex.x] = [];
    }

    if (typeof this.smallDroplets[arrayIndex.x][arrayIndex.y] === 'undefined') {
      this.smallDroplets[arrayIndex.x][arrayIndex.y] = [];
    }

    this.smallDroplets[arrayIndex.x][arrayIndex.y].push(droplet);
  }

  /**
   * Spawns a new large droplet on the screen based on the spawn chance
   * @param {number} width
   * @param {number} height
   * @return {void}
   */
  spawnNewLargeDroplet(width, height) {
    // If our random value doesn't match the given spawn rate, we don't spawn a droplet
    if (Math.random() > this.options.spawnRate.large) {
      return;
    }

    // Get a new droplet object from the pool
    const droplet = this.largeDropletContainer.get();

    // If the pool decided that we can't add more droplets, exit here
    if (droplet === null) {
      return;
    }

    // Make sure the droplet updates with a new position and radius
    const mass = Utils.getRandomInt(this.options.spawnMass.large.min, this.options.spawnMass.large.max);
    droplet.x = Utils.getRandomInt(0, width);
    droplet.y = Utils.getRandomInt(-100, height / 1.5);
    droplet.mass = mass / 2;
    droplet.targetMass = mass;
    droplet.width = droplet.mass * 6;
    droplet.height = droplet.mass * 7;
    droplet.dropletVelocity.x = 0;
    droplet.toBeRemoved = false;

    this.largeDroplets.push(droplet);
  }

  /**
   * Checks each droplet to see if it is positioned offscreen. If so, it's being pushed back into the object pool to be reused
   * @param {number} width
   * @param {number} height
   * @param {Array} dropletArray
   * @param {DropletPool} dropletContainer
   * @return {void}
   */
  static removeLargeOffscreenDroplets(width, height, dropletArray, dropletContainer) {
    // Store the length of the array so the for loop doesn't have to do that every run
    const length = dropletArray.length;

    for (let i = length - 1; i >= 0; i--) {
      if (dropletArray[i].x > width + 10 || dropletArray[i].x < -10 || dropletArray[i].y > height + 10 || dropletArray[i].y < -100) {
        dropletContainer.destroy(dropletArray[i]);
        dropletArray.splice(i, 1);
      }
    }
  }
}

/**
 * DropletPool class
 * Functions as an object pool so we can re-use droplets over and over again
 */
class DropletPool extends PIXI.particles.ParticleContainer {
  /**
   * DropletPool constructor
   */
  constructor(ObjectToCreate, objectTexture, startingSize, maximumSize) {
    super(maximumSize, {
      scale: true,
      position: true,
      rotation: false,
      uvs: false,
      alpha: false,
    });

    this.ObjectToCreate = ObjectToCreate;
    this.objectTexture = objectTexture;
    this.pool = [];
    this.inUse = 0;
    this.startingSize = startingSize;
    this.maximumSize = maximumSize;

    this.initialize();
  }

  /**
   * Initialize the initial batch of objects that we are going to use throughout the application
   * @return {void}
   */
  initialize() {
    for (let i = 0; i < this.startingSize; i += 1) {
      const droplet = new this.ObjectToCreate(this.objectTexture);
      droplet.x = -100;
      droplet.y = -100;
      droplet.anchor.set(0.5);

      // Add the object to the PIXI Container and store it in the pool
      this.addChild(droplet);
      this.pool.push(droplet);
    }
  }

  /**
   * Get an object from the object pool, checks whether there is an object left or it if may create a new object otherwise
   * @returns {object}
   */
  get() {
    // Check if we have reached the maximum number of objects, if so, return null
    if (this.inUse >= this.maximumSize) {
      return null;
    }

    // We haven't reached the maximum number of objects yet, so we are going to reuse an object
    this.inUse++;

    // If there are still objects in the pool return the last item from the pool
    if (this.pool.length > 0) {
      return this.pool.pop();
    }

    // The pool was empty, but we are still allowed to create a new object and return that
    const droplet = new this.ObjectToCreate(this.objectTexture);
    droplet.x = -100;
    droplet.y = -100;
    droplet.anchor.set(0.5, 0.5);

    // Add the object to the PIXI Container and return it
    this.addChild(droplet);
    return droplet;
  }

  /**
   * Put an element back into the object pool and reset it for later use
   * @param element - The object that should be pushed back into the object pool to be reused later on
   * @return {void}
   */
  destroy(element) {
    if (this.inUse - 1 < 0) {
      console.error('Something went wrong, you cant remove more elements than there are in the total pool');
      return;
    }

    // Move the droplet offscreen, we cant't set visible or rendering to false because that doesn't matter in a PIXI.ParticleContainer
    // @see: https://github.com/pixijs/pixi.js/issues/1910
    element.x = -100;
    element.y = -100;

    // Push the element back into the object pool so it can be reused again
    this.inUse -= 1;
    this.pool.push(element);
  }
}

/**
 * Droplet Class
 */
class Droplet extends PIXI.Sprite {
  /**
   * Droplet constructor
   */
  constructor(texture) {
    super(texture);

    this.mass = 0;
  }
}

/**
 * LargeDroplet Class
 */
class LargeDroplet extends Droplet {
  /**
   * Droplet constructor
   */
  constructor(texture) {
    super(texture);

    this.dropletVelocity = new PIXI.Point(0, 0);
    this.toBeRemoved = false;
    this.targetMass = 0;
  }
}

/**
 * Utilities Class has some functions that are needed throughout the entire application
 */
class Utils {
  /**
   * Returns a random integer between a given minimum and maximum value
   * @param {number} min - The minimum value, can be negative
   * @param {number} max - The maximum value, can be negative
   * @return {number}
   */
  static getRandomInt(min, max) {
    return Math.floor(Math.random() * ((max - min) + 1)) + min;
  }
}

/**
 * Onload function is executed whenever the page is done loading, initializes the application
 */
window.onload = () => {
  // Create a new instance of the application
  const application = new Application();
};

              
            
!
999px

Console