HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
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.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
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.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
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.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
html, body{
margin:0;
padding:0;
background-color:#ffffff;
}
canvas{
display:block;
position:absolute;
top:0;
left:0;
}
/**
* 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();
};
Also see: Tab Triggers