[mirotalk] - Improve room emoji reactions with floating burst animation

This commit is contained in:
Miroslav Pejic
2026-04-29 23:54:44 +02:00
parent 3867159a92
commit 271c709174
8 changed files with 153 additions and 21 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# ====================================================
# MiroTalk P2P v.1.8.23 - Environment Configuration
# MiroTalk P2P v.1.8.24 - Environment Configuration
# ====================================================
# App environment
+1 -1
View File
@@ -2,7 +2,7 @@
/**
* ==============================================
* MiroTalk P2P v.1.8.23 - Configuration File
* MiroTalk P2P v.1.8.24 - Configuration File
* ==============================================
*
* This file is the central configuration source.
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies: {
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.8.23
* @version 1.8.24
*
*/
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mirotalk",
"version": "1.8.23",
"version": "1.8.24",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mirotalk",
"version": "1.8.23",
"version": "1.8.24",
"license": "AGPL-3.0",
"dependencies": {
"@mattermost/client": "11.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mirotalk",
"version": "1.8.23",
"version": "1.8.24",
"description": "A free WebRTC browser-based video call",
"main": "server.js",
"scripts": {
+71 -4
View File
@@ -2805,11 +2805,78 @@ button {
}
.userEmoji {
z-index: 18;
z-index: 19;
position: fixed;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.user-emoji-burst {
position: absolute;
left: 15px;
bottom: 60px;
border-radius: 10px;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
color: #fff;
background: rgba(15, 23, 42, 0.34);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
box-shadow: 0 22px 44px rgba(0, 0, 0, 0.22);
backdrop-filter: blur(8px);
will-change: transform, opacity;
transform: translate3d(0, 0, 0) scale(0.82);
animation: roomEmojiFloat 4.8s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
.user-emoji-burst__icon {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: clamp(1.8rem, 1.2rem + 1.2vw, 2.7rem);
line-height: 1;
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.22));
}
.user-emoji-burst__name {
font-size: clamp(0.78rem, 0.68rem + 0.28vw, 0.95rem);
font-weight: 600;
letter-spacing: 0.01em;
white-space: nowrap;
}
@keyframes roomEmojiFloat {
0% {
opacity: 0;
transform: translate3d(0, 28px, 0) scale(0.78) rotate(var(--emoji-rotation, 0deg));
}
12% {
opacity: 1;
transform: translate3d(0, 0, 0) scale(1) rotate(var(--emoji-rotation, 0deg));
}
78% {
opacity: 1;
transform: translate3d(var(--emoji-drift, 0px), calc(var(--emoji-rise, -180px) * 0.84), 0) scale(1.04)
rotate(calc(var(--emoji-rotation, 0deg) * 0.4));
}
100% {
opacity: 0;
transform: translate3d(calc(var(--emoji-drift, 0px) * 1.15), var(--emoji-rise, -180px), 0) scale(1.08)
rotate(0deg);
}
}
@media (max-width: 640px) {
.user-emoji-burst {
gap: 8px;
padding: 8px 12px;
}
.user-emoji-burst__name {
max-width: 32vw;
overflow: hidden;
text-overflow: ellipsis;
}
}
/*--------------------------------------------------------------
+1 -1
View File
@@ -109,7 +109,7 @@ let brand = {
},
about: {
imageUrl: '../images/mirotalk-logo.gif',
title: 'WebRTC P2P v1.8.23',
title: 'WebRTC P2P v1.8.24',
html: `
<button
id="support-button"
+75 -10
View File
@@ -15,7 +15,7 @@
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.8.23
* @version 1.8.24
*
*/
@@ -313,6 +313,13 @@ const CHAT_REACTION_EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🔥'];
const CHAT_GPT_PEER_ID = 'chatgpt';
const CHAT_GPT_NAME = 'ChatGPT';
const roomEmojiBurstState = {
startedAt: 0,
anchorX: 0,
anchorY: 0,
count: 0,
};
// Chat room emoji picker
const msgerEmojiPicker = getId('msgerEmojiPicker');
@@ -12926,6 +12933,55 @@ document.addEventListener('click', (event) => {
msgerChat?.querySelectorAll('.reaction-picker').forEach((picker) => picker.remove());
});
function getRoomEmojiPlacement() {
const viewportWidth = Math.max(window.innerWidth || 0, 320);
const viewportHeight = Math.max(window.innerHeight || 0, 320);
const isCompactViewport = viewportWidth < 640;
const now = Date.now();
const burstWindow = 900;
const maxBurstSize = isCompactViewport ? 4 : 6;
const marginX = isCompactViewport ? 18 : 34;
const marginY = isCompactViewport ? 96 : 124;
const minAnchorX = viewportWidth * 0.2;
const maxAnchorX = viewportWidth * 0.8;
const minAnchorY = viewportHeight * 0.42;
const maxAnchorY = viewportHeight * 0.76;
if (now - roomEmojiBurstState.startedAt > burstWindow || roomEmojiBurstState.count >= maxBurstSize) {
roomEmojiBurstState.startedAt = now;
roomEmojiBurstState.count = 0;
roomEmojiBurstState.anchorX = minAnchorX + Math.random() * Math.max(1, maxAnchorX - minAnchorX);
roomEmojiBurstState.anchorY = minAnchorY + Math.random() * Math.max(1, maxAnchorY - minAnchorY);
}
const burstIndex = roomEmojiBurstState.count;
roomEmojiBurstState.count += 1;
const baseAngle = -90 + (burstIndex - (maxBurstSize - 1) / 2) * (isCompactViewport ? 24 : 18);
const jitterAngle = Math.random() * 12 - 6;
const angle = ((baseAngle + jitterAngle) * Math.PI) / 180;
const radius = (isCompactViewport ? 18 : 24) + burstIndex * (isCompactViewport ? 14 : 18) + Math.random() * 14;
const left = Math.min(
viewportWidth - marginX,
Math.max(marginX, roomEmojiBurstState.anchorX + Math.cos(angle) * radius)
);
const top = Math.min(
viewportHeight - marginY,
Math.max(marginY, roomEmojiBurstState.anchorY + Math.sin(angle) * radius * 0.6)
);
const drift = `${(Math.cos(angle) * (radius * 0.95) + (Math.random() * 18 - 9)).toFixed(0)}px`;
const rise = `-${(Math.abs(Math.sin(angle)) * 70 + Math.random() * 70 + (isCompactViewport ? 120 : 165)).toFixed(0)}px`;
const rotation = `${(Math.random() * 16 - 8).toFixed(1)}deg`;
return {
left,
top,
drift,
rise,
rotation,
};
}
/**
* Handle room emoji reaction
* @param {object} message
@@ -12934,14 +12990,23 @@ document.addEventListener('click', (event) => {
function handleEmoji(message, duration = 5000) {
if (userEmoji) {
const emojiDisplay = document.createElement('div');
emojiDisplay.className = 'animate__animated animate__backInUp';
emojiDisplay.style.padding = '10px';
emojiDisplay.style.fontSize = '2vh';
emojiDisplay.style.color = '#FFF';
emojiDisplay.style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
emojiDisplay.style.borderRadius = '10px';
emojiDisplay.style.marginBottom = '5px';
emojiDisplay.innerText = `${message.emoji} ${message.peer_name}`;
const placement = getRoomEmojiPlacement();
const label = message.peer_name || 'Guest';
const emojiIcon = document.createElement('span');
const emojiName = document.createElement('span');
emojiDisplay.className = 'user-emoji-burst';
emojiDisplay.style.left = `${placement.left}px`;
emojiDisplay.style.top = `${placement.top}px`;
emojiDisplay.style.setProperty('--emoji-drift', placement.drift);
emojiDisplay.style.setProperty('--emoji-rise', placement.rise);
emojiDisplay.style.setProperty('--emoji-rotation', placement.rotation);
emojiIcon.className = 'user-emoji-burst__icon';
emojiIcon.textContent = message.emoji;
emojiName.className = 'user-emoji-burst__name';
emojiName.textContent = label;
emojiDisplay.appendChild(emojiIcon);
emojiDisplay.appendChild(emojiName);
userEmoji.appendChild(emojiDisplay);
setTimeout(() => {
@@ -15580,7 +15645,7 @@ function showAbout() {
Swal.fire({
background: swBg,
position: 'center',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.23',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.24',
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
customClass: { image: 'img-about' },
html: `