diff --git a/twitch/customEmoteWall.js b/twitch/customEmoteWall.js
new file mode 100644
index 0000000..72d0e33
--- /dev/null
+++ b/twitch/customEmoteWall.js
@@ -0,0 +1,76 @@
+(function() {
+
+ function getEmotes(event, message){
+ var emotes = event.getTags().get('emotes'),
+ str = message,
+ i;
+
+ if (emotes.length() > 0) {
+ emotes = emotes.replaceAll('[0-9]+:', '').split('/');
+ for (i in emotes) {
+ str = str.replace(getWordAt(message, parseInt(emotes[i].split('-')[0])), '');
+ }
+ }
+ return str;
+ }
+
+
+ function sendData(tpe, data) {
+ $.panelsocketserver.sendJSONToAll(JSON.stringify({
+ 'eventFamily': 'emote',
+ 'eventType': tpe,
+ 'data': data
+ }));
+ }
+
+ $.bind('ircChannelMessage', function(event){
+ var sender = event.getSender(),
+ message = event.getMessage().toLowerCase(),
+ messageLength = message.length(),
+ tags = event.getTags();
+ $.consoleLn(message)
+ $.consoleLn(tags)
+ })
+
+ $.bind('command', function(event) {
+
+ const sender = "" + event.getSender().toLowerCase(),
+ command = event.getCommand(),
+ args = event.getArgs(),
+ action = args[0],
+ value = args[1];
+
+ if (command.equalsIgnoreCase('overlay')) {
+ if (!action) {
+ $.say($.whisperPrefix(sender) + $.lang.get('customOverlay.help', ' Use "!overlay [follow | subscribe | donation | timer] value" to set overlay data.'));
+ } else if (action.equalsIgnoreCase('follow')) {
+ sendData(action, value);
+ } else if (action.equalsIgnoreCase('subscribe')) {
+ sendData(action, value);
+ } else if (action.equalsIgnoreCase('donation')) {
+ sendData(action, value);
+ } else if (action.equalsIgnoreCase('timer')) {
+ sendData(action, new Date(Date.now().getTime()+value*1000*60));
+ } else {
+ $.say($.whisperPrefix(sender) + $.lang.get('customOverlay.help'));
+ }
+ }
+ });
+
+ $.bind('twitchFollow', function(event) {
+ sendData('follow', event.getFollower());
+ });
+ $.bind('twitchSubscriber', function(event) {
+ sendData('subscribe', event.getSubscriber());
+ });
+
+ $.bind('initReady', function() {
+ $.registerChatCommand('./custom/custom/customOverlay.js', 'overlay');
+
+ $.registerChatSubcommand('overlay', 'follow', 2);
+ $.registerChatSubcommand('overlay', 'subscribe', 2);
+ $.registerChatSubcommand('overlay', 'donation', 2);
+ $.registerChatSubcommand('overlay', 'timer', 2);
+ });
+
+})();
\ No newline at end of file
diff --git a/web/emotewall/index.html b/web/emotewall/index.html
new file mode 100644
index 0000000..b88b793
--- /dev/null
+++ b/web/emotewall/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+ EmoteWall
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/emotewall/index.js b/web/emotewall/index.js
new file mode 100644
index 0000000..a2d1147
--- /dev/null
+++ b/web/emotewall/index.js
@@ -0,0 +1,258 @@
+$(function () {
+
+ const SEVENTV_GLOBAL_URL = "https://api.7tv.app/v2/emotes/global";
+ const SEVENTV_CHANNEL_URL = "https://api.7tv.app/v2/users/" + "..." + "/emotes";
+ const FFZ_GLOBAL_URL = "https://api.frankerfacez.com/v1/set/global";
+ const FFZ_CHANNEL_URL = "https://api.frankerfacez.com/v1/room/" + "...";
+ const BTTV_GLOBAL_URL = "https://api.betterttv.net/3/cached/emotes/global";
+ const BTTV_CHANNEL_URL = "https://api.betterttv.net/3/cached/users/twitch/" + "...";
+
+ const webSocket = window.socket;
+ const emote_str = ["peepo", 'nod']
+ const emote_url = ['https://cdn.7tv.app/emote/603cac391cd55c0014d989be/2x', 'https://cdn.7tv.app/emote/60ae4bb30e35477634610fda/1x']
+ var img_map = {};
+
+ var lastframe = 0;
+ var preloaded = false, initialized = false;
+ var mode = "rain" || "bounce" || "explosion" || "firework" || "...";
+
+ var particles = [];
+
+
+ const canvas = document.getElementById('wall');
+ const ctx = canvas.getContext('2d');
+ const resizeCanvas = () => {
+ canvas.width = window.innerWidth-8;
+ canvas.height = window.innerHeight-8;
+ }
+ window.addEventListener('resize', resizeCanvas, false);
+ resizeCanvas();
+
+ function loadImages(strs, urls) {
+ let loadcount = 0, loadtotal = urls.length;
+ preloaded = false;
+ var loadedimages = {};
+ for (let i=0; i (t<0 || t>8 )? 0 : (t<2 ? t/2 : (t>6 ? (8-t)/2 : 1))
+
+ class Particle {
+ constructor(emote,data){
+ this.size = 64;
+ this.data = data;
+ this.x = Math.random() * canvas.width - this.size;
+ this.y = Math.random() * canvas.height - this.size;
+ this.vx = this.vy = 0;
+ this.ax = this.ay = 0;
+ this.emote = emote;
+ this.time = 0;
+ particles.push(this);
+ setTimeout(()=>{particles.pop()}, 10000);
+ }
+
+ draw(){
+ ctx.globalAlpha = tWave(this.time);
+ if(img_map[this.emote])
+ ctx.drawImage(img_map[this.emote], this.x, this.y, this.size, this.size);
+ }
+
+ update(tick){
+ this.time += tick;
+ this.vx += tick * this.ax;
+ this.vy += tick * this.ay;
+ this.x += tick * this.vx * part_speed;
+ this.y += tick * this.vy * part_speed;
+ }
+ }
+
+
+ class RainP extends Particle {
+ constructor(emote,data) {
+ super(emote,data)
+ this.x = Math.random() * canvas.width;
+ this.y = - this.size;
+ this.vx = 0;
+ this.vy = 15;
+ this.ax = 0, this.ay = 0;
+ this.time = 2;
+ }
+ update(tick){
+ super.update(tick)
+ }
+ }
+
+ class BounceP extends Particle {
+ constructor(emote,data) {
+ super(emote,data)
+ this.x = Math.random() * canvas.width;
+ this.y = Math.random() * canvas.height;
+ this.vx = (Math.random() * 2 - 1) *15;
+ this.vy = (Math.random() * 2 - 1) * 15;
+ this.ax = 0, this.ay = 0;
+ }
+
+ update(tick){
+ super.update(tick);
+ if (this.x < 0) {
+ this.vx = this.vx < 0 ? -this.vx * part_bounce_el : this.vx;
+ this.x = 0;
+ } else if (this.x + this.size > canvas.width) {
+ this.vx = this.vx > 0 ? -this.vx * part_bounce_el : this.vx;
+ this.x = canvas.width - this.size;
+ }
+ if (this.y <0) {
+ this.vy = this.vy < 0 ? -this.vy * part_bounce_el : this.vy;
+ this.y = 0;
+ } else if (this.y + this.size > canvas.height) {
+ this.vy = this.vy > 0 ? -this.vy * part_bounce_el : this.vy;
+ this.y = canvas.height - this.size;
+ }
+ }
+ }
+
+ class BounceGP extends BounceP {
+ constructor(emote,data) {
+ super(emote,data)
+ this.x = Math.random() * canvas.width;
+ this.y = Math.random() * canvas.height;
+ this.vx = Math.random() * 20 - 10;
+ this.vy = Math.random() * 20 - 10;
+ this.ax = 0, this.ay = 10;
+ }
+
+ update(tick){
+ super.update(tick);
+ }
+ }
+
+ class FireworkP extends Particle {
+ constructor(emote,data) {
+ super(emote,data)
+ this.x = canvas.width/2;
+ this.y = canvas.height;
+ this.vx = 0;
+ this.vy = -15;
+ this.ax = 0;
+ this.ay = 0;
+ this.time=4;
+ this.boom = false;
+ }
+
+ update(tick){
+ super.update(tick);
+ if(this.y <= canvas.height/4 && !this.boom){
+ let angle = Math.random() *2 * Math.PI;
+ this.vx = Math.sin(angle) * (Math.random()*4 + 10);
+ this.vy = Math.cos(angle) * (Math.random()*4 + 10);
+ this.ay = 10;
+ this.boom = true;
+ }
+ }
+ }
+
+
+
+
+ function init() {
+
+ new RainP('peepo', {});
+ new RainP('peepo', {});
+ new RainP('peepo', {});
+ new RainP('peepo', {});
+ new RainP('peepo', {});
+ new BounceGP('nod', {});
+ new BounceP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ new FireworkP('nod', {});
+ main(0);
+ }
+
+ function main(tframe) {
+ window.requestAnimationFrame(main);
+
+ var dt = (tframe - lastframe) / 1000;
+ lastframe = tframe;
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ if (!initialized) {
+ if (preloaded) {
+ setTimeout(function(){initialized = true;}, 1000);
+ }
+ } else {
+ particles.forEach((particle) => {
+ particle.update(dt);
+ });
+ particles.forEach((particle) => {
+ particle.draw();
+ });
+ }
+ }
+
+ init()
+
+ const handleSocketMessage = (e)=>{
+ try {
+ let rawMessage = e.data,
+ message = JSON.parse(rawMessage);
+
+ if(!message.hasOwnProperty('eventFamily') || message.eventFamily != 'emotewall' ||
+ !message.hasOwnProperty('eventType') || !message.hasOwnProperty('data'))
+ return;
+
+ console.log(message.eventType, message.data)
+ if(message.eventType == 'message') {
+ //....
+ }
+ } catch (ex) {
+ console.log(ex)
+ }
+ };
+
+
+ jQuery(async ()=>{
+
+ try{
+ socket.addFamilyHandler("overlay", handleSocketMessage);
+ if(socket){
+ while(socket.getReadyState() === 0){
+ await new Promise(r => setTimeout(r, 500));
+ }
+ }
+ }catch(e) {console.log(e)}
+ })
+
+});
\ No newline at end of file