---
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");
}
});
});
};
```