前言

给 Twikoo 换一套表情,再加个鼠标悬停放大预览。表情列表和样式还在,放大脚本这次单独拆出来,后面维护也方便一些。

美化之前

本站使用 Hexo 6.0.0 + Butterfly 4.5.1,其他版本请按实际情况调整。样式、脚本引入及全局变量用法可参考:

参考链接

  • Mo:Twikoo 添加自定义表情包。
  • Leonus 的表情放大教程:https://blog.leonus.cn/2022/owo-big.html(原链接暂不可用)。
  • Heo:CSS 动画原作者,代码中保留署名。

修改方案

1. 准备表情 JSON

把表情数据保存到 source/static/json/OwO.json。下面从本站现有文件抽取了少量条目,格式不变;图片素材的使用仍需遵守各自来源要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"颜文字": {
"type": "emoticon",
"container": [
{
"icon": "OωO",
"text": "Author: DIYgod"
},
{
"icon": "|´・ω・)ノ",
"text": "Hi"
}
]
},
"Bilibili": {
"type": "image",
"container": [
{
"icon": "<img src=\"https://owo.imaegoo.com/bilibili/6ea59c827c414b4a2955fe79e0f6fd3dcd515e24.png\">",
"text": "tv_doge"
},
{
"icon": "<img src=\"https://owo.imaegoo.com/bilibili/a8111ad55953ef5e3be3327ef94eb4a39d535d06.png\">",
"text": "tv_亲亲"
}
]
}
}

emoticon 用于文字表情,image 的 icon 则是图片 HTML。只填写自己维护和信任的内容,不要把任意来访者提交的 HTML 直接拼进去。

部署后先确认 /static/json/OwO.json 可以直接返回 JSON,再在 Twikoo 管理设置的 EMOTION_CDN 填入自己站点这个文件的完整 URL。该设置属于后端管理配置,不能通过加一条 CSS 代替。

2. 添加样式

新建 source/static/css/owo-preview.css,添加下面的样式。如果之前改过这一块,记得先合并旧样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#owo-big {
position: fixed;
align-items: center;
background-color: rgb(255, 255, 255);
border: 1px #aaa solid;
border-radius: 10px;
z-index: 9999;
display: none;
transform: translate(0, -105%);
overflow: hidden;
animation: owoIn 0.3s cubic-bezier(0.42, 0, 0.3, 1.11);
}

.OwO .OwO-body .OwO-items .OwO-item:hover {
box-shadow: 0 2px 3px 1px rgb(0 0 0 / 15%)!important;
}

[data-theme=dark] #owo-big {
background-color: #4a4a4a
}

#owo-big img {
width: 100%;
}

/* 动画效果代码由 Heo:https://blog.zhheo.com/ 提供 */
@keyframes owoIn {
0% {
transform: translate(0, -95%);
opacity: 0;
}
100% {
transform: translate(0, -105%);
opacity: 1;
}
}

然后在 _config.butterfly.yml 的 inject.head 里引入:

1
2
3
inject:
head:
- <link rel="stylesheet" href="/static/css/owo-preview.css">

3. 恢复预览逻辑

旧实现通过 MutationObserver 等待表情 DOM,依赖具体的新增节点顺序。下面是保留“停留 300ms、放大约三倍”效果的这版代码,改为全局事件委托,只处理鼠标,不接管手机长按。

4. 添加脚本

新建 source/static/js/owo-preview.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
(() => {
if (window.__juneOwoPreviewBound) return;
window.__juneOwoPreviewBound = true;
let timer;
let box;
const hide = () => {
clearTimeout(timer);
if (box) box.style.display = 'none';
};
document.addEventListener('pointerover', event => {
if (event.pointerType !== 'mouse') return;
const image = event.target.closest && event.target.closest('#twikoo-wrap .OwO-item img');
if (!image || image.contains(event.relatedTarget)) return;
hide();
timer = setTimeout(() => {
if (!image.isConnected) return;
if (!box || !box.isConnected) {
box = document.createElement('div');
box.id = 'owo-big';
box.style.pointerEvents = 'none';
document.body.appendChild(box);
}
const rect = image.getBoundingClientRect();
const width = Math.min(Math.max(rect.width * 3, 64), innerWidth - 20);
const preview = document.createElement('img');
preview.src = image.currentSrc || image.src;
preview.alt = image.alt || '表情预览';
box.replaceChildren(preview);
box.style.cssText = 'display:block;position:fixed;pointer-events:none;width:' + width + 'px;';
box.style.left = Math.max(10, Math.min(rect.left - width / 3, innerWidth - width - 10)) + 'px';
box.style.top = Math.max(width + 10, rect.top) + 'px';
}, 300);
});
document.addEventListener('pointerout', event => {
if (event.target.closest && event.target.closest('#twikoo-wrap .OwO-item img')) hide();
});
document.addEventListener('pjax:send', hide);
addEventListener('scroll', hide, true);
})();

然后在 _config.butterfly.yml 的 inject.bottom 里引入。

1
2
3
inject:
bottom:
- <script defer src="/static/js/owo-preview.js"></script>

如果和我一样用了 JS 自动注入,就在 source/static/js/_inject.yml 的 bottom 里添加文件名,不要重复引入。这里绑定了全局事件的脚本设为 pjax: false,避免切换页面时重复绑定。

脚本使用 DOM 方法设置图片,避免把地址再次拼成 HTML。以后升级表情组件时,记得看看 .OwO-item img 这个选择器是否还对应表情图片。