// ==UserScript==
// @name Animate Emoji for Drawaria
// @namespace Tampermonkey
// @version 3.0
// @description Adds a stylish, draggable menu with an internal data source for selecting animated emojis and stamping them on the Drawaria.online canvas.
// @author YouTubeDrawaria / Gemini
// @homepage https://github.com/quarrel/animate-web-emoji
// @match https://drawaria.online/
// @match https://*.drawaria.online/*
// @match https://drawaria.online/test
// @match https://drawaria.online/room/*
// @run-at document-idle
// @icon https://fonts.gstatic.com/s/e/notoemoji/latest/1f603/512.webp
// @license MIT
// @noframes
// @resource LOTTIE_BACKUP_PUREJS_PLAYER_URL https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie_canvas.min.js
// @grant GM.getResourceUrl
// @grant GM.addStyle
// @grant GM.xmlHttpRequest
// @connect cdn.jsdelivr.net
// ==/UserScript==
/* globals lottie */
(function () {
'use strict';
const scriptStartTime = Date.now();
const config = {
DEBUG_MODE: false,
UNIQUE_EMOJI_CLASS: 'animated-emoji-q',
};
let emojiData = []; // New structure to hold all emoji data
let codepointToLottie = new Map();
let isPureLottiePlayerLoaded = false;
// --- DATOS INTERNOS DE EMOJIS (HARDCODED) ---
// C: Codepoint | E: Emoji Character | D: Description (for search)
const ANIMATED_EMOJIS_DATA = [
// Smileys and Emotion
{ c: '1f600', e: '😀', d: 'Smile' }, { c: '1f603', e: '😃', d: 'Smile big eyes' },
{ c: '1f604', e: '😄', d: 'Grin' }, { c: '1f601', e: '😁', d: 'Grinning' },
{ c: '1f606', e: '😆', d: 'Laughing' }, { c: '1f605', e: '😅', d: 'Grin sweat' },
{ c: '1f602', e: '😂', d: 'Joy tears' }, { c: '1f923', e: '🤣', d: 'Rofl' },
{ c: '1f62d', e: '😭', d: 'Loudly crying' }, { c: '1f609', e: '😉', d: 'Wink' },
{ c: '1f617', e: '😗', d: 'Kissing' }, { c: '1f619', e: '😙', d: 'Kissing smiling eyes' },
{ c: '1f61a', e: '😚', d: 'Kissing closed eyes' }, { c: '1f618', e: '😘', d: 'Kissing heart' },
{ c: '1f970', e: '🥰', d: 'Heart face' }, { c: '1f60d', e: '😍', d: 'Heart eyes' },
{ c: '1f929', e: '🤩', d: 'Star struck' }, { c: '1f973', e: '🥳', d: 'Partying face' },
{ c: '1fae0', e: '🫠', d: 'Melting' }, { c: '1f643', e: '🙃', d: 'Upside down' },
{ c: '1f642', e: '🙂', d: 'Slightly happy' }, { c: '1f972', e: '🥹', d: 'Happy cry' },
{ c: '1f979', e: '🥺', d: 'Holding back tears' }, { c: '1f60a', e: '😊', d: 'Blush' },
{ c: '1f631', e: '😱', d: 'Scream fear' }, { c: '1f60c', e: '😌', d: 'Relieved' },
{ c: '1f60b', e: '😋', d: 'Yummy food' }, { c: '1f61b', e: '😛', d: 'Tongue' },
{ c: '1f911', e: '🤑', d: 'Money face' }, { c: '1f974', e: '🤯', d: 'Exploding head' },
{ c: '1f92a', e: '🤪', d: 'Crazy wacky' }, { c: '1f92b', e: '🤫', d: 'Shushing' },
{ c: '1f92d', e: '🤦', d: 'Facepalm' }, { c: '1f97a', e: '🥺', d: 'Pleading face' },
{ c: '1f64f', e: '🙏', d: 'Praying hands' }, { c: '1f44f', e: '👏', d: 'Clapping hands' },
// Hands and Symbols
{ c: '1f44d', e: '👍', d: 'Thumbs up' }, { c: '1f44e', e: '👎', d: 'Thumbs down' },
{ c: '1f44c', e: '👌', d: 'OK hand' }, { c: '1f44a', e: '✊', d: 'Raised fist' },
{ c: '1f496', e: '💖', d: 'Sparkling heart' }, { c: '1f499', e: '💙', d: 'Blue heart' },
{ c: '1f4af', e: '💯', d: 'Hundred points' }, { c: '1f525', e: '🔥', d: 'Fire flame hot' },
{ c: '1f389', e: '🎉', d: 'Party popper' }, { c: '1f4a9', e: '💩', d: 'Poop pile' },
{ c: '1f31f', e: '🌟', d: 'Glowing star' }, { c: '1f47d', e: '👽', d: 'Alien monster' },
];
// --- UTILITIES & INITIALIZATION ---
function loadScript(url, id, callback) {
if (document.getElementById(id)) {
if (callback) callback();
return;
}
const script = document.createElement('script');
script.id = id;
script.src = url;
script.onload = () => {
if (id === 'lottie-canvas-player') isPureLottiePlayerLoaded = true;
if (callback) callback();
};
document.head.appendChild(script);
}
async function getLottieAnimationData(codepoint) {
if (codepointToLottie.has(codepoint)) {
return codepointToLottie.get(codepoint);
}
try {
// Animation JSONs are consistently hosted here by the original project
const lottieUrl = `https://cdn.jsdelivr.net/gh/quarrel/noto-emoji-animation-web/emoji/${codepoint}.json`;
// Use GM.xmlHttpRequest for cross-origin fetch for reliable data loading
const response = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: lottieUrl,
onload: (res) => resolve(res),
onerror: (err) => reject(err),
});
});
const data = JSON.parse(response.responseText);
codepointToLottie.set(codepoint, data);
return data;
} catch (error) {
if (config.DEBUG_MODE) {
console.error(
`🇦🇺: Failed to fetch Lottie JSON for ${codepoint}. This emoji cannot be stamped.`,
error
);
}
return null;
}
}
// Simplified initialization: just copy the hardcoded data
function initializeEmojiData() {
emojiData = ANIMATED_EMOJIS_DATA;
}
// --- DRAGGABLE MENU IMPLEMENTATION ---
const MENU_ID = 'animated-emoji-menu';
const MENU_TITLE = '🎨 Emojis Animados';
let selectedEmojiCodepoint = null;
function createDraggableMenu() {
const menu = document.createElement('div');
menu.id = MENU_ID;
menu.innerHTML = `
<div class="menu-header">
${MENU_TITLE}
<span class="close-btn">×</span>
</div>
<div class="menu-content">
<input type="text" id="emoji-search" placeholder="Buscar emoji (ej: heart, smile)..." title="Busca por el emoji o su descripción (ej: heart, dog)">
<div id="emoji-gallery"></div>
</div>
`;
document.body.appendChild(menu);
// Make it draggable
const header = menu.querySelector('.menu-header');
header.addEventListener('mousedown', initDrag, false);
menu.querySelector('.close-btn').addEventListener('click', () => {
menu.style.display = 'none';
});
// Initialize drag functionality
let drag = false,
offsetX,
offsetY;
function initDrag(e) {
if (e.button !== 0) return;
drag = true;
offsetX = e.clientX - menu.offsetLeft;
offsetY = e.clientY - menu.offsetTop;
document.addEventListener('mousemove', doDrag, false);
document.addEventListener('mouseup', stopDrag, false);
e.preventDefault();
}
function doDrag(e) {
if (drag) {
let newLeft = e.clientX - offsetX;
let newTop = e.clientY - offsetY;
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - menu.offsetWidth));
newTop = Math.max(0, Math.min(newTop, window.innerHeight - menu.offsetHeight));
menu.style.left = newLeft + 'px';
menu.style.top = newTop + 'px';
}
}
function stopDrag() {
drag = false;
document.removeEventListener('mousemove', doDrag, false);
document.removeEventListener('mouseup', stopDrag, false);
}
// Add a button to toggle the menu
const toggleBtn = document.createElement('button');
toggleBtn.id = 'toggle-emoji-menu-btn';
toggleBtn.textContent = '🎨 Emojis Animados';
toggleBtn.onclick = () => {
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
};
document.body.appendChild(toggleBtn);
return menu;
}
function populateEmojiGallery(menu) {
const gallery = menu.querySelector('#emoji-gallery');
gallery.innerHTML = '';
// Use the guaranteed internal data
for (const item of emojiData) {
const emojiItem = document.createElement('span');
emojiItem.className = 'emoji-item';
emojiItem.textContent = item.e; // The actual emoji character
emojiItem.dataset.codepoint = item.c;
emojiItem.title = item.d; // Description for hover and search
// Pre-fetch Lottie data asynchronously to speed up stamping
getLottieAnimationData(item.c).catch(() => {});
emojiItem.addEventListener('click', () => {
selectEmojiForDrawing(emojiItem);
});
gallery.appendChild(emojiItem);
}
// Add search functionality
const searchInput = menu.querySelector('#emoji-search');
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
gallery.querySelectorAll('.emoji-item').forEach((item) => {
const description = item.title.toLowerCase();
const emojiChar = item.textContent;
// Search by the emoji character or the description
const isMatch =
description.includes(searchTerm) ||
emojiChar.includes(searchTerm);
item.style.display = isMatch ? 'inline-block' : 'none';
});
});
}
function selectEmojiForDrawing(emojiItem) {
document.querySelectorAll('.emoji-item.selected').forEach((item) => {
item.classList.remove('selected');
});
emojiItem.classList.add('selected');
selectedEmojiCodepoint = emojiItem.dataset.codepoint;
if (config.DEBUG_MODE) {
console.log(
'🇦🇺: Emoji selected for drawing:',
emojiItem.textContent
);
}
// Show a temporary message to the user
const menu = document.getElementById(MENU_ID);
if (menu) {
const message = document.createElement('div');
message.textContent = `Seleccionado: ${emojiItem.textContent}. 🖌️ Haz clic en el canvas para estampar.`;
message.style.cssText = 'position: absolute; bottom: 0; left: 0; right: 0; background: #98c379; color: #282c34; padding: 5px; text-align: center; border-radius: 0 0 12px 12px; font-size: 14px; font-weight: bold; animation: fadein 0.5s;';
message.classList.add('selection-message');
// Remove any previous message and add the new one
menu.querySelectorAll('.selection-message').forEach(m => m.remove());
menu.appendChild(message);
// Auto-hide the message after 2.5 seconds
setTimeout(() => message.remove(), 2500);
}
}
// --- CANVAS STAMP FUNCTIONALITY ---
async function stampEmojiOnCanvas(e) {
if (!selectedEmojiCodepoint) return;
const canvas = document.getElementById('canvas');
if (!canvas) return;
// Ensure the Lottie Pure JS Player is loaded before proceeding
if (!isPureLottiePlayerLoaded) {
const LOTTIE_BACKUP_PUREJS_PLAYER_URL = await GM.getResourceUrl('LOTTIE_BACKUP_PUREJS_PLAYER_URL');
loadScript(LOTTIE_BACKUP_PUREJS_PLAYER_URL, 'lottie-canvas-player', () => stampEmojiOnCanvas(e));
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) return;
const animationData = await getLottieAnimationData(selectedEmojiCodepoint);
if (!animationData) return;
// --- RENDER LOGIC ---
const tempCanvas = document.createElement('canvas');
const emojiSize = 72; // Larger stamp size
tempCanvas.width = emojiSize;
tempCanvas.height = emojiSize;
// The Lottie player is globally available now:
const player = lottie.loadAnimation({
renderer: 'canvas',
loop: false,
autoplay: false,
animationData: animationData,
container: tempCanvas,
rendererSettings: {
context: tempCanvas.getContext('2d'),
preserveAspectRatio: 'xMidYMid meet',
clearCanvas: true,
hideOnTransparent: true,
},
});
player.addEventListener('loaded', () => {
// Render the first frame
player.goToAndStop(0, true);
// Get mouse coordinates relative to the main canvas
const rect = canvas.getBoundingClientRect();
// Center the stamp on the click position
const x = e.clientX - rect.left - emojiSize / 2;
const y = e.clientY - rect.top - emojiSize / 2;
// Draw the rendered frame onto the main Drawaria canvas
ctx.drawImage(tempCanvas, x, y, emojiSize, emojiSize);
// Clean up
player.destroy();
tempCanvas.remove();
if (config.DEBUG_MODE) console.log('🇦🇺: Emoji stamped successfully.');
});
player.addEventListener('error', (err) => {
if (config.DEBUG_MODE) console.error('🇦🇺: Lottie rendering error:', err);
player.destroy();
});
}
function attachCanvasListener() {
const canvas = document.getElementById('canvas');
if (canvas) {
canvas.addEventListener('click', stampEmojiOnCanvas);
if (config.DEBUG_MODE)
console.log('🇦🇺: Canvas listener attached.');
} else {
setTimeout(attachCanvasListener, 500);
}
}
// --- MAIN FUNCTION & STYLES ---
const main = () => {
try {
initializeEmojiData();
// 1. Initialize Menu and Canvas Interaction
const menu = createDraggableMenu();
populateEmojiGallery(menu);
attachCanvasListener();
// 2. Load the essential Lottie player asynchronously
GM.getResourceUrl('LOTTIE_BACKUP_PUREJS_PLAYER_URL').then(url => {
loadScript(url, 'lottie-canvas-player');
});
// 3. Add Styles
GM.addStyle(`
/* --- STYLES FOR DRAGGABLE MENU --- */
#${MENU_ID} {
position: fixed;
top: 50px;
left: 50px;
width: 320px;
height: 450px;
min-width: 200px;
min-height: 200px;
background: #282c34;
border: 1px solid #61afef;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.7);
z-index: 99999;
display: none;
resize: both;
overflow: hidden;
color: #abb2bf;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
transition: box-shadow 0.2s;
}
#${MENU_ID}:hover {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.9), 0 0 10px #61afef80;
}
#${MENU_ID} .menu-header {
cursor: grab;
padding: 12px;
background: #3e4451;
color: #c678dd;
font-weight: bold;
font-size: 1.1em;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #61afef;
}
#${MENU_ID} .menu-header:active {
cursor: grabbing;
}
#${MENU_ID} .close-btn {
cursor: pointer;
font-size: 1.5em;
line-height: 1;
color: #e06c75;
transition: color 0.2s;
}
#${MENU_ID} .close-btn:hover {
color: #ff0000;
}
#${MENU_ID} .menu-content {
padding: 10px;
height: calc(100% - 47px);
display: flex;
flex-direction: column;
}
#emoji-search {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #555;
background: #1e2127;
color: #ffffff;
border-radius: 6px;
font-size: 14px;
}
#emoji-gallery {
flex-grow: 1;
overflow-y: auto;
border: 1px solid #3e4451;
padding: 5px;
border-radius: 6px;
background: #21252b;
}
#emoji-gallery::-webkit-scrollbar {
width: 8px;
}
#emoji-gallery::-webkit-scrollbar-thumb {
background-color: #565d6c;
border-radius: 10px;
}
.emoji-item {
cursor: pointer;
font-size: 28px;
padding: 4px;
margin: 3px;
border-radius: 6px;
display: inline-block;
transition: background-color 0.1s, transform 0.1s;
line-height: 1;
}
.emoji-item:hover {
background-color: #3e4451;
transform: scale(1.1);
}
.emoji-item.selected {
border: 2px solid #98c379;
background-color: #546a48;
box-shadow: 0 0 5px #98c379;
}
/* --- TOGGLE BUTTON STYLES --- */
#toggle-emoji-menu-btn {
position: fixed;
top: 10px;
right: 10px;
z-index: 100000;
padding: 8px 15px;
cursor: pointer;
background: #56b6c2;
color: #1e2127;
font-weight: bold;
border: none;
border-radius: 6px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
transition: background-color 0.2s, transform 0.1s;
}
#toggle-emoji-menu-btn:hover {
background: #61afef;
transform: translateY(-2px);
}
/* Keyframes for selection message */
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
`);
if (config.DEBUG_MODE) {
console.log(
'🇦🇺: ',
'Script startup time: ' +
(Date.now() - scriptStartTime) +
'ms'
);
}
} catch (error) {
if (config.DEBUG_MODE) {
console.error(
'🇦🇺: ',
'Failed to initialize emoji animation script:',
error
);
}
}
};
main();
})();