fancy-webgl-sparkles
Version:
PIXI.js library to add glitter particles with bokeh and special effects to your DOM elements
559 lines (498 loc) • 30 kB
JavaScript
/*! fancy-webgl-sparkles 1.0.5 | (c) 2021 Eli Menendez | Apache License */
const FancyWebGLSparkles = (() =>
{
class FancyWebGLSparkles
{
constructor(element, inSettings = {})
{
if (!(element instanceof Node)) throw "Can't initialize FancyWebGLSparkles because " + element + " is not a Node.";
this.settings = inSettings;
this.element = element;
this.mouseEventElement = null;
this.pixi = null;
//This is the spritesheet image as base64, feel free to change the spritesheet with your own sprites.
//Tip: if you wish to generate your own textures use shoebox or texture packer to package the assets, remember to put pixijs as ouput format and inline your base64 spritesheet and json data below.
//Frames 0 to 6 are used for the animated sparkles, frames 7, 9 and 10 are used for bokeh and frame 9 is used for the stars
this.sprites = "data:image/png;base64,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";
this.spriteSheetData =
{
"frames":
{
"s1.png":
{
"frame": {"x":98, "y":112, "w":37, "h":35},
"spriteSourceSize": {"x":12,"y":14,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
"s10.png":
{
"frame": {"x":0, "y":112, "w":48, "h":49},
"spriteSourceSize": {"x":8,"y":8,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
"s2.png":
{
"frame": {"x":49, "y":112, "w":48, "h":41},
"spriteSourceSize": {"x":8,"y":12,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
"s3.png":
{
"frame": {"x":109, "y":56, "w":52, "h":55},
"spriteSourceSize": {"x":5,"y":5,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
"s4.png":
{
"frame": {"x":0, "y":0, "w":56, "h":55},
"spriteSourceSize": {"x":4,"y":5,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
"s5.png":
{
"frame": {"x":55, "y":56, "w":53, "h":55},
"spriteSourceSize": {"x":5,"y":5,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
"s6.png":
{
"frame": {"x":111, "y":0, "w":37, "h":35},
"spriteSourceSize": {"x":11,"y":11,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
"s7.png":
{
"frame": {"x":136, "y":112, "w":26, "h":25},
"spriteSourceSize": {"x":18,"y":19,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
"s8.png":
{
"frame": {"x":0, "y":56, "w":54, "h":54},
"spriteSourceSize": {"x":5,"y":5,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
"s9.png":
{
"frame": {"x":57, "y":0, "w":53, "h":55},
"spriteSourceSize": {"x":5,"y":5,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
}
},
"meta":
{
"image": this.sprites,
"size": {"w": 163, "h": 162},
"scale": "1"
}
};
for (let property in this.getDefaultSettings())
{
let attrSetting = this.element.getAttribute("sparkle-" + property);
//If this property exists in the user settings, set the object property to that setting, otherwise use the defaults
if (property in inSettings) this.settings[property] = inSettings[property];
else if (attrSetting != "" && attrSetting != null)
{
try
{
this.settings[property] = JSON.parse(attrSetting);
} catch (e)
{
this.settings[property] = attrSetting;
}
}
else this.settings[property] = this.getDefaultSettings()[property];
}
this.addEventListeners();
}
getDefaultSettings()
{
return{
//colors to use for the sparkles
sparkleColor: ["#ffffff","#ffff00", "#e15ecb", "#32e187"],
//Rendering controls allow you to enable or disable bokeh, sparkles or stars
renderBokeh: false,
renderSparkles: true,
renderStars: true,
//sparkle particle count
sparkleScale: 50,
//simulation speed
speed: 2,
//minimum particle size
minSize: .05,
//maximum particle size
maxSize: .16,
//direction of sparkle particles, you can use up, down or both
direction: "both",
//If set to true allows particles to render outside from the element's bounding box
renderOutside: true,
//Array of colors for the bokeh effect
bokehColor: ["#ffffff","#ffff00"],
//This scale is proportional to the number of sparkles on the screen to avoid pollution
bokehScale: 1.5,
//Size multiplier to scale the bokeh, IE a scale of two means double the size
bokehSize: .7,
//This scale is proportional to the number of sparkles on the screen
starScale: 2,
//Size multiplier to scale the star particles, IE a scale of two means double the original size
starSize: 1,
//Scale of the boundary if renderOutside is set to true, IE a value of 2 would double the size of the area that is being rendered outside of the parent element boundaries.
boundaryScale: 1,
//If this setting is true the particles will start rendering as soon as the DOM is generated.
persistent: false
};
}
//Static Constructor
static init(elements, settings)
{
if (elements instanceof Node) elements = [elements];
if (elements instanceof NodeList) elements = [].slice.call(elements);
if (!(elements instanceof Array)) return;
elements.forEach(element =>
{
if (!("FancyWebGLSparkles" in element)) element.FancyWebGLSparkles = new FancyWebGLSparkles(element, settings);
});
}
addEventListeners()
{
if(!this.settings.persistent)
{
this.element.addEventListener("mouseenter", (e) =>
{
this.mouseEventElement = e;
this.start(e.target);
});
this.element.addEventListener("mouseleave", ()=>
{
if(this.pixi === null || this.pixi === undefined) return;
this.pixi.bIsPendingDestroy = true;
});
return;
}
this.start(this.element);
}
//Stop the pixi instance
stop()
{
this.pixi.stop();
this.pixi.bInstanceHasBeenInitialized = true;
}
start(element)
{
this.width = this.settings.renderOutside? (this.element.getBoundingClientRect().width * 1.4) * this.settings.boundaryScale: this.element.getBoundingClientRect().width;
this.height = this.settings.renderOutside? (this.element.getBoundingClientRect().height * 1.4) * this.settings.boundaryScale : this.element.getBoundingClientRect().height;
if(this.pixi == undefined)
{
this.pixi = new PIXI.Application({
width: this.width, height: this.height, transparent: true, autoDensity: true, clearBeforeRender: true
});
}
if(this.pixi.renderer.context.isLost)
{
this.pixi.destroy(true,
{
children: true,
texture: false,
baseTexture: false
});
this.pixi = new PIXI.Application({
width: this.width, height: this.height, transparent: true, autoDensity: true, clearBeforeRender: true
});
this.pixi.bInstanceHasBeenInitialized = undefined;
}
//Append the pixi instance on top of the container and center with the help of css
const pixiNode = element.appendChild(this.pixi.view);
pixiNode.style.position = "absolute";
pixiNode.style.left = "50%";
pixiNode.style.top = "50%";
pixiNode.style.transform = "translate(-50%, -50%)";
//Add this property to later allow space for memory management once the particles are not being rendered on the screen
this.pixi.bIsPendingDestroy = false;
//We want to avoid any collisions between the canvas and the DOM
pixiNode.style.pointerEvents = "none";
//Find the parent and check if there's a stacking context to position the canvas on top of the element and not to the top of the root document, in our case we will set static DOM elements as relative to add the stacking context
//https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
const parentNode = window.getComputedStyle(element).position;
if(parentNode === "static") element.style.position = "relative";
//Add webgl filters
const filter = new PIXI.filters.AdvancedBloomFilter(.05, 50, 4, 4, 4, null, 1, PIXI.settings.RESOLUTION);
this.pixi.stage.filters = [filter];
this.pixi.stage.filterArea = this.pixi.screen;
//Load texture assets to the vRam cache pool if the cache is empty
const spritesheetUrl = `data:application/json;base64,${btoa(JSON.stringify(this.spriteSheetData))}`;
if(Object.keys(PIXI.utils.TextureCache).length === 0)
this.pixi.loader.add("spritesheet", spritesheetUrl).load(() => this.onTexturesLoaded(false));
else this.onTexturesLoaded(true);
}
//Construct the particles with its initial properties after the pixi instance and its textures have been allocated on memory
onTexturesLoaded(bTexturesAlreadyOnMemory)
{
//If we have lost the context because we have hit webgl limits then we need to create the context again
if (bTexturesAlreadyOnMemory && this.pixi.bInstanceHasBeenInitialized != undefined)
{
this.pixi.start();
return;
}
const textureStore = [];
const spriteStoreOne = [];
const spriteStoreTwo = [];
const spriteStoreThree = [];
for(let i = 0; i < 10; ++i) textureStore.push(PIXI.Texture.from(`s${i + 1}.png`));
//FadeIn the particles on creation for a nice smooth effect =)
this.pixi.stage.alpha = 0;
// create our sparkle sprite using the datauri supplied in the script
const particleTypeOne = new PIXI.ParticleContainer(Math.round(this.settings.sparkleScale / 10 * this.settings.bokehScale),{
scale: true,
vertices: true,
position: true,
rotation: false,
uvs: true,
alpha: true
});
const particleTypeTwo = new PIXI.ParticleContainer(Math.round(this.settings.sparkleScale),{
scale: true,
vertices: true,
position: true,
rotation: false,
uvs: true,
alpha: true
});
const particleTypeThree = new PIXI.ParticleContainer(Math.round(this.settings.sparkleScale / 8 * this.settings.starScale),{
scale: true,
vertices: true,
position: true,
rotation: true,
uvs: true,
alpha: true
});
//This intermediate container is created to allow for webgl effects to be applied to the sparkle particles and get this nice glow effect.
const particleContainerOne = new PIXI.Container();
particleContainerOne.addChild(particleTypeTwo);
const theFilter = new PIXI.filters.GlowFilter(10, 1, 0, PIXI.utils.string2hex("#ffffff"), .1);
particleContainerOne.filters = [theFilter];
particleContainerOne.filterArea = this.pixi.renderer.screen;
//Create Bokeh Particles
for(let i = 0; i < Math.round((this.settings.sparkleScale * 0.10) * this.settings.bokehScale); ++i)
{
const bokeh = Math.round(Math.random())? new PIXI.Sprite(textureStore[6]) : new PIXI.Sprite(textureStore[9]);
bokeh.scale.set((Math.random() * (1.2 * this.settings.bokehSize)) + (0.4 * this.settings.bokehSize));
bokeh.blendMode = PIXI.BLEND_MODES.OVERLAY;
bokeh.tint = this.settings.bokehColor === "rainbow"? Math.random() * 0xE8D4CD : PIXI.utils.string2hex(this.settings.bokehColor[Math.floor(Math.random() * this.settings.bokehColor.length)]);
bokeh.x = Math.random() * this.pixi.screen.width;
bokeh.y = Math.random() * this.pixi.screen.height;
bokeh.anchor.set(0.5, 0.5);
//Initial Fade state, fadeIn = 1, fadeOut = 0
bokeh.fade = Math.round(Math.random());
bokeh.alpha = this.clamp(Math.random(), 0.1, 0.7);
spriteStoreOne.push(bokeh);
particleTypeOne.addChild(bokeh);
}
//Create Sparkle Particles
for(let i = 0; i < Math.round(this.settings.sparkleScale); ++i)
{
const sparkle = PIXI.AnimatedSprite.fromFrames(["s1.png","s2.png", "s3.png", "s4.png", "s5.png", "s6.png", "s7.png"]);
sparkle.tint = this.settings.sparkleColor === "rainbow"? Math.random() * 0xE8D4CD : PIXI.utils.string2hex(this.settings.sparkleColor[Math.floor(Math.random() * this.settings.sparkleColor.length)]);
sparkle.x = Math.random() * this.pixi.screen.width;
sparkle.y = Math.random() * this.pixi.screen.height;
sparkle.anchor.set(0.5, 0.5);
sparkle.blendMode = PIXI.BLEND_MODES.HARD_LIGHT;
sparkle.alpha = this.clamp(Math.random(), 0.3, 1);
sparkle.fade = Math.round(Math.random());
sparkle.rotation = Math.random() * Math.PI;
sparkle.scale.set(this.clamp(Math.random() * this.settings.maxSize, this.settings.minSize, this.settings.maxSize));
sparkle.gotoAndPlay(Math.floor(Math.random() * 6));
sparkle.animationSpeed = .18;
const xDirection = Math.floor(Math.random() * 20) - 10;
const yDirection = this.settings.direction === "up"? Math.floor(Math.random() * 5) - 5.5 : this.settings.direction === "down"? Math.floor(Math.random() * 5) + .5 : Math.floor(Math.random() * 10) - 5;
sparkle.direction =
{
//Add some randomness to the direction of movement on construction
x: xDirection,
y: yDirection
};
spriteStoreTwo.push(sparkle);
particleTypeTwo.addChild(sparkle);
}
//Create Star Particles
for(let i = 0; i < Math.round((this.settings.sparkleScale * .25) * this.settings.starScale); ++i)
{
const star = new PIXI.Sprite(textureStore[7]);
star.tint = PIXI.utils.string2hex("#ffffff");
star.x = Math.random() * this.pixi.screen.width;
star.y = Math.random() * this.pixi.screen.height;
star.anchor.set(0.5, 0.5);
star.alpha = this.clamp(Math.random(), 0.3, 1);
star.fade = Math.round(Math.random());
star.rotation = Math.random() * Math.PI;
star.zoom = Math.floor(Math.random());
star.scale.set(this.clamp(Math.random() * this.settings.maxSize * 1.5, this.settings.minSize * 1.5, this.settings.maxSize * 1.5));
const xDirection = Math.floor(Math.random() * 20) - 10;
const yDirection = this.settings.direction === "up"? Math.floor(Math.random() * 5) - 5.5 : this.settings.direction === "down"? Math.floor(Math.random() * 5) + .5 : Math.floor(Math.random() * 10) - 5;
star.direction =
{
//Add some randomness to the direction of movement on particle construction
x: xDirection,
y: yDirection
};
spriteStoreThree.push(star);
particleTypeThree.addChild(star);
}
//Render particles based on user choice
if(this.settings.renderBokeh) this.pixi.stage.addChild(particleTypeOne);
if(this.settings.renderSparkles) this.pixi.stage.addChild(particleContainerOne);
if(this.settings.renderStars) this.pixi.stage.addChild(particleTypeThree);
//Initialize the update function
this.pixi.ticker.add(()=>
{
this.fadeInCanvas();
this.fadeOutCanvas();
if(this.pixi != null && !this.pixi.bIsPendingDestroy) this.update(spriteStoreOne, spriteStoreTwo, spriteStoreThree);
});
}
//simple utility function used to matematically clamp diferent parameters
clamp(value,min,max) { return value > max ? max : value < min ? min : value; }
//FadeIn the content when the mouse enters the view
fadeInCanvas()
{
if (this.pixi.stage.alpha >= 1 || this.pixi.bIsPendingDestroy) return;
this.pixi.stage.alpha = this.clamp(this.pixi.stage.alpha, 0, 1);
this.pixi.stage.alpha += 0.05 * this.pixi.ticker.deltaTime;
}
//FadeOut the content when the mouse gets out of the view before destroying the instance
fadeOutCanvas()
{
if (this.pixi.stage.alpha === 0 && !this.pixi.bIsPendingDestroy) return;
if (this.pixi.stage.alpha >= 0 && this.pixi.bIsPendingDestroy)
{
this.pixi.stage.alpha = this.clamp(this.pixi.stage.alpha, 0, 1);
this.pixi.stage.alpha -= 0.08 * this.pixi.ticker.deltaTime;
}
if(this.pixi.stage.alpha <= 0 && this.pixi.bIsPendingDestroy) this.stop(this.mouseEventElement);
}
//Pulse the scale of the particle
pulseParticle(particle, maxScale = 1.2, minScale = 0)
{
const theScale = particle.scale.x;
//Similar to fading
// 1 = Zoom In 0 = Zoom Out
particle.zoom = (theScale >= maxScale && particle.zoom) ? 0 : (theScale <= minScale && !particle.zoom) ? 1 : particle.zoom;
//zoom in
if(particle.zoom) particle.scale.set(particle.scale.x + .003 * this.pixi.ticker.deltaTime);
//zoom out
if(!particle.zoom) particle.scale.set(particle.scale.x - .003 * this.pixi.ticker.deltaTime);
//Clamp values to not allow negatives
particle.scale.x = this.clamp(particle.scale.x, minScale, maxScale);
particle.scale.y = this.clamp(particle.scale.y, minScale, maxScale);
}
//Fade In and out the particle, randomizing its position after it has been faded out
fadeParticle(particle, maxOpacity = 0.6, newMinimumScale = .4, newMaximumScale = 1.2)
{
//Set particle fade state 1 = Fade In 0 = Fade Out
particle.fade = (particle.alpha >= maxOpacity && particle.fade) ? 0 : (particle.alpha <= 0 && !particle.fade) ? 1 : particle.fade;
//Fade in
if(particle.fade) particle.alpha += .003 * this.pixi.ticker.deltaTime;
//Fade out
if(!particle.fade) particle.alpha -= .003 * this.pixi.ticker.deltaTime;
//Clamp values to not allow negatives
particle.alpha = this.clamp(particle.alpha, 0, maxOpacity);
//Check if the particle has faded out, change its position
if(!particle.fade && particle.alpha == 0)
{
particle.x = Math.random() * this.pixi.screen.width;
particle.y = Math.random() * this.pixi.screen.height;
particle.scale.set((Math.random() * (newMaximumScale - newMinimumScale)) + newMinimumScale);
}
}
// Position particles at the other end of the canvas whenever they hit the bounds of the canvas itself
// We also avoid clipping by setting a boundary based on the canvas size
throwParticlesBackToTheCanvas(particle)
{
const boundingBox =
{
x: this.element.getBoundingClientRect().width,
y: this.element.getBoundingClientRect().height
};
//Prevent clipping by providing an invisible boundary area for particles
const bounds =
{
xMin: () => this.settings.renderOutside? boundingBox.x * 0.1 : 0,
xMax: () => this.settings.renderOutside? this.width - (boundingBox.x * 0.1) : this.width,
yMin: () => this.settings.renderOutside? boundingBox.y * 0.1 : 0,
yMax: () => this.settings.renderOutside? this.height - (boundingBox.y * 0.1) : this.height,
};
if(particle.x > bounds.xMax() || particle.x < bounds.xMin() || particle.y > bounds.yMax() || particle.y < bounds.yMin())
{
//Resize Particles if they are out of bounds
particle.scale.set(this.clamp(Math.random() * this.settings.maxSize, this.settings.minSize, this.settings.maxSize));
}
if(particle.x > bounds.xMax()) particle.x = this.settings.renderOutside? bounds.xMin : 0;
if(particle.x < bounds.xMin()) particle.x = this.settings.renderOutside? bounds.xMax : this.width;
// if the particles have hit the vertical bounds, teleport them to a new X position with the y position inverted
if(particle.y > bounds.yMax())
{
particle.y = this.settings.renderOutside? bounds.yMin : 0;
particle.x = Math.floor(Math.random() * bounds.xMax);
}
if(particle.y < bounds.yMin())
{
particle.y = this.settings.renderOutside? bounds.yMax : this.height;
particle.x = Math.floor(Math.random() * bounds.xMax);
}
}
//Update loop on delta
update(bokehs, sparkles, stars)
{
sparkles.forEach((sparkle) =>
{
this.throwParticlesBackToTheCanvas(sparkle);
this.fadeParticle(sparkle, 1, this.settings.minSize, this.settings.maxSize);
// Randomly move stars along the direction, we weight x heavier than y,
// allowing space for random decelleration, giving an ethereal floating feeling
const speed =
{
x: () =>
{
const randBool = Math.random() > Math.random() * 2;
return randBool? this.settings.speed / 20 : 0;
},
y: () =>
{
const randBool = Math.random() > Math.random() * 5;
return randBool? this.settings.speed / 10 : this.settings.speed / 15;
},
};
//Perform the position update
sparkle.x += speed.x() * sparkle.direction.x * this.pixi.ticker.deltaTime;
sparkle.y += speed.y() * sparkle.direction.y * this.pixi.ticker.deltaTime;
});
bokehs.forEach((bokeh)=>
{
this.throwParticlesBackToTheCanvas(bokeh);
this.fadeParticle(bokeh, 0.6, 0.4 * this.settings.bokehSize, 1.2 * this.settings.bokehSize);
//Move the bokeh particle
//The scale is going to be pumped on both the x and y axis at the same time, therefore we don't care of discrepancies between both axis to measure the current scale
let currScale = bokeh.scale.x;
currScale += 0.001 * this.pixi.ticker.deltaTime;
bokeh.y -= (Math.random() * 0.2) * this.pixi.ticker.deltaTime;
bokeh.scale.set(currScale);
});
stars.forEach((star)=>
{
this.throwParticlesBackToTheCanvas(star);
this.fadeParticle(star, 1, this.settings.minSize, this.settings.maxSize);
//Perform the position update
star.x += (this.settings.speed / 50 * star.direction.x) * this.pixi.ticker.deltaTime;
star.y += (this.settings.speed / 30 * star.direction.y) * this.pixi.ticker.deltaTime;
star.rotation += star.direction.y * this.pixi.ticker.deltaTime * Math.PI * .01;
this.pulseParticle(star, this.settings.maxSize * 1.5, 0.1);
});
}
}
if (typeof document !== "undefined")
{
/* expose the class to window and autoload */
window.FancyWebGLSparkles = FancyWebGLSparkles;
FancyWebGLSparkles.init(document.querySelectorAll("[sparkle]"));
}
return FancyWebGLSparkles;
})();