emojiwall
Version:
On-screen emotes/emojis nicely animated, bubbling up from the bottom of the screen, to the top of the screen
204 lines (170 loc) • 8.93 kB
JavaScript
var particle_list = document.getElementById('particles');
var emojiwallConfiguration = applyConfigurationDefaults(null);
function applyConfigurationDefaults(forConfiguration=null) {
return {
'canvasId': forConfiguration?.canvasId ?? 'particles',
'maxEmojis': Math.abs(forConfiguration?.maxEmojis ?? 300),
'maxCrossingTime': Math.abs(forConfiguration?.maxCrossingTime ?? 18),
'minCrossingTime': Math.abs(forConfiguration?.minCrossingTime ?? 6),
'maxEmojiSize': Math.abs(forConfiguration?.maxEmojiSize ?? 140),
'minEmojiSize': Math.abs(forConfiguration?.minEmojiSize ?? 60),
'effectRotation': {
'enabled': forConfiguration?.effectRotation?.enabled ?? false,
'maxRotation': forConfiguration?.effectRotation?.maxRotation ?? 25,
}
};
}
function initializeEmojiwall(withConfiguration=null) {
if(typeof withConfiguration !== 'object') {
console.warn('Cannot initialize emojiwall. withconfiguration is not an object.');
return;
}
emojiwallConfiguration = applyConfigurationDefaults(withConfiguration);
// Check that minimums are smaller than maximums
emojiwallConfiguration.minCrossingTime = Math.min(emojiwallConfiguration.minCrossingTime, emojiwallConfiguration.maxCrossingTime);
emojiwallConfiguration.minEmojiSize = Math.min(emojiwallConfiguration.minEmojiSize, emojiwallConfiguration.maxEmojiSize);
particle_list = document.getElementById(emojiwallConfiguration.canvasId);
// Y initial placement or emojis.
updateStartTopStyle();
// If the container is resized, we need to update the initial Y position of the emojis, to make sure they spawn off screen.
const canvasResizeObserver = new ResizeObserver(() => {
updateStartTopStyle();
} );
canvasResizeObserver.observe(particle_list);
if(particle_list === null) {
console.warn(`Cannot initialize emojiwall. a container with Id "${withConfiguration.canvasId}" was not found.`);
return;
}
console.debug('🎉 Successfully initialized emojiwall with settings', emojiwallConfiguration);
}
/* Set the initial top position of the emojis, below the bottom edge of the viewport */
function updateStartTopStyle() {
// Remove the previously set style first
document.querySelectorAll('.emojiwall-initial-style').forEach(style => style.remove());
// Calculate and set the new style
const startTopPosition = document.getElementById(emojiwallConfiguration.canvasId).offsetHeight + (emojiwallConfiguration.maxEmojiSize / 4);
const initialEmojiStyle = document.createElement('style');
initialEmojiStyle.classList.add('emojiwall-initial-style');
initialEmojiStyle.rel = 'stylesheet';
initialEmojiStyle.innerHTML = `.particles .emoji_item { top: ${startTopPosition}px; }`;
document.head.appendChild(initialEmojiStyle);
}
function removeEmojiElement(elem) {
elem.remove();
}
const intersectionCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting === false) {
removeEmojiElement(entry.target);
} else {
if (emojiwallConfiguration.effectRotation.enabled === true) {
setTimeout(() => {
rotateAnimation(entry.target);
}, parseInt(entry.target.style.transitionDelay.replace('ms', '')) + 100);
}
}
});
}
const intersectionObserverOptions = {
root: document.getElementById('particles'),
rootMargin: '500px',
threshold: 0.1
}
const intersectionObserver = new IntersectionObserver(intersectionCallback, intersectionObserverOptions);
function setRandomLeftPosition(element) {
// Randomize start Y position
element.style.left = getRandomNumber(0, 100) + '%';
}
function getRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function rotateAnimation(element) {
const start_angle = element.style.transform.replace('rotate(', '').replace('deg)', '');
let end_angle = getRandomNumber(0, emojiwallConfiguration.effectRotation.maxRotation);
// We want all rotations to cross the 0deg mark
if (start_angle > 0) {
end_angle *= -1;
}
const animation_rotation = [{
transform: `rotate(${start_angle}deg)`
},
{
transform: `rotate(${end_angle}deg)`
}]
// get transition duration for element
const transition_duration = element.style.transitionDuration.split(',')[0].replace('s', '');
const rotation_duration = getRandomNumber(transition_duration * 1100, transition_duration * 1300);
const animation_rotation_timing = {
duration: rotation_duration,
iterations: 1,
}
element.animate(animation_rotation, animation_rotation_timing);
}
function addEmojis(emojis, image_host) {
// example 'emojis' array :
// ['😀', '🎅', '/img/parrot.gif']
const new_emojis = [];
emojis.forEach(function (element) {
if (particle_list.childElementCount >= emojiwallConfiguration.maxEmojis) {
// remove oldest emoji
removeEmojiElement(particle_list.firstElementChild);
}
var particle_item = null;
if (element.startsWith('/') === false) {
// UTF-8 emojis
// we wrap each emoji in their own element
const emoji_span = document.createElement('span');
emoji_span.classList.add('emoji_item');
emoji_span.innerHTML = element;
particle_item = emoji_span;
} else {
// Image emojis
const emoji_image = document.createElement('img');
emoji_image.setAttribute('src', `${image_host}${element}`)
emoji_image.classList.add('emoji_item');
particle_item = emoji_image;
}
// set emoji X start position
setRandomLeftPosition(particle_item);
particle_list.appendChild(particle_item);
new_emojis.push(particle_item);
});
setTimeout(function () {
for (let i = 0; i < new_emojis.length; i++) {
// Randomize 'end' Y position
let left_position = getRandomNumber(0, 100);
// If the new emoji spawned at the far right, make sure we don't
// keep it leeching the right side: we force it to have a end X position
// that is somewhat more left. This is to avoid emojis that stay partly
// off screen during all their transition.
// Same thing applies to the left side.
const start_position = parseInt(new_emojis[i].style.left.replace('%', ''));
if ( start_position > 90 || start_position < 0) {
left_position = getRandomNumber(20, 80);
}
// Randomize speed!
const duration = getRandomNumber(emojiwallConfiguration.minCrossingTime, emojiwallConfiguration.maxCrossingTime);
// Randomize the emoji size
const size = getRandomNumber(emojiwallConfiguration.minEmojiSize, emojiwallConfiguration.maxEmojiSize);
// Randomize delay before the transition starts
const delay = getRandomNumber(0, 5000);
// Randomize easing function
const easing_functions = ['ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear'];
const easing = easing_functions[getRandomNumber(0, easing_functions.length - 1)];
// If the rotation effect is enabled, we need to add the rotation style
const rotation_effect = emojiwallConfiguration.effectRotation.enabled ? `transform: rotate(${getRandomNumber(emojiwallConfiguration.effectRotation.maxRotation * -1, emojiwallConfiguration.effectRotation.maxRotation)}deg);` : '';
const common_style = `left: ${left_position}%; top: -${500+(emojiwallConfiguration.maxEmojiSize*2)}px; transition: top ${duration}s ${easing} ${delay}ms, left ${duration}s ${easing} ${delay}ms;${rotation_effect}`;
// If the emoji is an image, we need to set the height of the element
// (Setting the height on the span has side effects, that's why the styles are separate)
const img_style = `height: ${size}px;`;
// If the emoji is a span, we need to set the font-size of the element
// (Not sure if setting the font-size on the image has side effects, but just in case)
const span_style= `font-size: ${size}px;`;
// Apply the appropriate styles
new_emojis[i].style = common_style + (new_emojis[i].tagName === 'IMG' ? img_style : span_style);
// Start observing the emoji, so we can remove it from the DOM when it's no longer visible
intersectionObserver.observe(new_emojis[i]);
}
// Wait 50ms, so that the initial CSS has time to be applied before we apply the transition css
}, 50);
}