--- title: "利用 Streamlabs 的聊天室擷取自訂功能,讓畫面更生動!" tags: OBS description: "" --- {%hackmd @f6bfb5/biKBG-dnSQajGm3NZj38Fg %} ![](https://badgen.net/badge/created/2021-11-25/) ![](https://badgen.net/badge/last-modify/2021-11-25/) - [總覽](/s/1lhI9SqjRzepld5qyOMHKw) ## 前言 前陣子幫[P眼](https://www.twitch.tv/alexasseye910062)做了個隨機在所有對話者前面加圖的聊天室機制,也順手摸了一些 Streamlabs 的 Chat Box 有什麼可以玩的,藉此文順便做個簡單紀錄。 ![center](https://i.imgur.com/hsRWilo.png) <span class="center">▲每個觀眾名字前面都會隨機出現不同的精靈球。</span> <style> img[alt="center"], .center { display: block; margin: auto; } .center { text-align: center; } </style> [Streamlabs](https://streamlabs.com/dashboard) 的聊天室擷取(於「All Widgets」裡的「Chat Box」)可以透過撰寫 CSS 與 JavaScript(將頁面最下方的「Enable Custom HTML/CSS」設為「Enabled」開啟)添加各種不同變化。 本文預設環境為「Theme」設定成「Twitch」之後進行修改。 CSS 部分需保留初始程式碼,無需做任何改動,於最上方新增空行後再貼上以下程式碼即可。 ## CSS - 改為點陣風格的字體 ```css= @import "https://fontlibrary.org//face/gnu-unifont"; body { font-family: 'UnifontUpperMedium', 'Unifont' !important; } ``` ## CSS - 加上半透明淺灰色背景 可視需求加入。 ```css= body { background-color: rgba(24,24,24,.33) !important; } ``` ## CSS - 修改吃字問題 原始版面設定下名稱與訊息為固定寬度,並且縮得太小時會以刪節號省略,將其改為處於同一行內的彈性寬度。 ```css= #log .message,#log .meta { display: inline !important; } #log .meta { padding-right: 0 !important; white-space: normal !important; text-overflow: inherit !important; overflow: inherit !important; } .badge:last-child { margin-right: 0 !important; } .name { margin-left: 0 !important; } ``` ## CSS - 替名稱和訊息加上外框 ### 白框 ```css= /* White stroke */ .name, #log .message { text-shadow: -2px -2px #e7e7e7, -2px -1px #e7e7e7, -2px 0px #e7e7e7, -2px 1px #e7e7e7, -2px 2px #e7e7e7, -1px -2px #e7e7e7, -1px -1px #e7e7e7, -1px 0px #e7e7e7, -1px 1px #e7e7e7, -1px 2px #e7e7e7, 0px -2px #e7e7e7, 0px -1px #e7e7e7, 0px 0px #e7e7e7, 0px 1px #e7e7e7, 0px 2px #e7e7e7, 1px -2px #e7e7e7, 1px -1px #e7e7e7, 1px 0px #e7e7e7, 1px 1px #e7e7e7, 1px 2px #e7e7e7, 2px -2px #e7e7e7, 2px -1px #e7e7e7, 2px 0px #e7e7e7, 2px 1px #e7e7e7, 2px 2px #e7e7e7; } ``` ### 黑框 ```css= /* Black stroke */ .name, #log .message { text-shadow: -2px -2px #232323, -2px -1px #232323, -2px 0px #232323, -2px 1px #232323, -2px 2px #232323, -1px -2px #232323, -1px -1px #232323, -1px 0px #232323, -1px 1px #232323, -1px 2px #232323, 0px -2px #232323, 0px -1px #232323, 0px 0px #232323, 0px 1px #232323, 0px 2px #232323, 1px -2px #232323, 1px -1px #232323, 1px 0px #232323, 1px 1px #232323, 1px 2px #232323, 2px -2px #232323, 2px -1px #232323, 2px 0px #232323, 2px 1px #232323, 2px 2px #232323; } ``` ## JS - 在人名前方加上隨機圖片 ```javascript= // 實況主替換圖片 let broadcasterIcon = "PASTE_YOUR_IMAGE_URL_HERE"; // MOD 替換圖片 let moderatorIcon = "PASTE_YOUR_IMAGE_URL_HERE"; // MOD 隨機圖片 let moderatorIconArray = [ "PASTE_YOUR_IMAGE_URL_HERE", ]; // 觀眾隨機圖片 let imgUrlArray = [ "PASTE_YOUR_IMAGE_URL_HERE", ]; let imgNodeArray = []; function preloadImages() { let imgPlaceholder = document.createElement('div'); document.querySelector("body").append(imgPlaceholder); imgPlaceholder.style.width = "0px"; imgPlaceholder.style.height = "0px"; for(let i=0; i<imgUrlArray; i++) { imgPlaceholder.style.backgroundImage = imgUrlArray[i]; } if(document.images) { for(let i=0; i<imgUrlArray.length; i++) { let imgPlaceholder = new Image(); imgPlaceholder.src = imgUrlArray[i]; imgPlaceholder.classList.add('badge'); imgNodeArray.push(imgPlaceholder); } } } function getRandomEleFromArr(arr) { return arr[Math.floor(Math.random()*arr.length)]; } function randomImageNode() { return getRandomEleFromArr(imgNodeArray).cloneNode(true); } // Please use event listeners to run functions. document.addEventListener('onLoad', function(obj) { // obj will be empty for chat widget // this will fire only once when the widget loads preloadImages(); }); document.addEventListener('onEventReceived', function(obj) { // obj will contain information about the event let messageId = obj.detail.messageId; // 更改或加入徽章圖示 let badgesString = obj.detail.tags.badges; if(badgesString != undefined) { if (badgesString.includes("broadcaster/1")) { document.querySelector('div[data-id="' + messageId + '"] .badges img.broadcaster-icon').src = broadcasterIcon; } else if(badgesString.includes("moderator/1")) { document.querySelector('div[data-id="' + messageId + '"] .badges img.type-icon').src = getRandomEleFromArr(moderatorIconArray); } else { document.querySelector('div[data-id="' + obj.detail.messageId + '"] .badges').append(randomImageNode()); } } }); ``` ## JS - 將觀眾顯示名稱從暱稱改為帳號 ```javascript= document.addEventListener('onEventReceived', function(obj) { // ... let messageId = obj.detail.messageId; // 於此函式插入以下程式碼 let userId = obj.detail.from; // 將觀眾顯示名稱從暱稱改為帳號 if (messageId != null) document.querySelector('div[data-id="' + messageId + '"] span.meta span.name').innerText = userId; // ... } ``` ## JS - 替顯示名稱加入雜訊顯示特效 ```html= <!-- item will be appened to this layout --> <div id="log" class="sl__chat__layout"> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js" integrity="sha512-z4OUqw38qNLpn1libAN9BsoDx6nbNFio5lA6CuTp9NlK83b89hgyCVq+N5FdBJptINztxn1Z3SaKSKUS5UP60Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <!-- chat item --> <script type="text/template" id="chatlist_item"> <div data-from="{from}" data-id="{messageId}"> <span class="meta" style="color: {color}"> <span class="badges"> </span> <span class="name">{from}</span> </span> <span class="message"> {message} </span> </div> </script> ``` ```css= /* https://www.uiui.dev/ */ .meta { position: relative; } .meta span:nth-child(2), .meta span:nth-child(3) { position: absolute; /* 精確位置需依使用字體再行調整 * 本文使用範例為 Unifont */ top: -8px !important; width: 100%; height: 100%; } .meta span:nth-child(2) { /* 精確位置需依使用字體再行調整 * 本文使用範例為 Unifont */ left: -15px; text-shadow: -1px 0 #ff00c1; clip: rect(44px, 450px, 56px, 0); } .meta span:nth-child(3) { /* 精確位置需依使用字體再行調整 * 本文使用範例為 Unifont */ left: -17px; text-shadow: -1px 0 #00fff9, 1px 1px #ff00c1; } ``` ```javascript= document.addEventListener('onEventReceived', function(obj) { // ... // 於此函式插入以下程式碼 // 取自 https://www.uiui.dev/ if (messageId != null) { const glitchText = document.querySelector('div[data-id="' + messageId + '"] span.meta span.name'); const glitchTextWrap = document.querySelector('div[data-id="' + messageId + '"] span.meta'); glitchTextWrap.innerHTML = new Array(3) .fill(0) .map( (_, i) => `<span class="${i > 0 ? "name-layer" : "name"}">${ glitchText.textContent }</span>` ) .join(" "); const layers = [ ...document.querySelectorAll('div[data-id="' + messageId + '"] span.meta .name-layer') ]; setTimeout(() => { const keyframes = () => new Array(20).fill(0).map(() => ({ clip: `rect(${Math.random() * 90}px, 9999px, ${ Math.random() * 90 }px, 0)`, skew: `${Math.random() * 6 - 6}deg` })); revealHeading({ elem: layers[0], config: { loop: true, easing: "steps(2)", duration: 10000, keyframes: keyframes(), translateZ: 0 } }); revealHeading({ elem: layers[1], config: { loop: true, easing: "steps(2)", duration: 5000, keyframes: keyframes(), translateZ: 0 } }); }, 500); } // ... } const revealHeading = ({ elem, config }) => { return new Promise((resolve) => { anime({ targets: elem, easing: "spring(1, 60, 15, 3)", ...config, update: (a) => { a.progress > 10 && resolve(elem); elem.classList.add("Revealed"); } }); }); }; ```