前言:为自己站点配置雷姆live2d,并在live2d模型的基础上附加添加看板功能。
安装hexo-helper-live2d
- 下载
hexo-helper-live2d
(https://github.com/EYHN/hexo-helper-live2d)
npm install --save hexo-helper-live2d
配置自定义蕾姆模型
文件配置
- remu模型下载:https://onedrive.wztlink1013.com/zh-CN/live2d/ 【密码:remu】
- 在hexo项目根目录新建
live2d_models
文件夹,再将所下载的模型解压放入该文件夹
_config.yml配置
- 根目录_config.yml添加如下配置
- API文档:https://l2dwidget.js.org/docs/class/src/index.js~L2Dwidget.html#instance-method-init
## live2d
live2d:
enable: true # 是否开启live2d
scriptFrom: local # 脚本从本地引入
pluginRootPath: live2d/ # 插件在站点上的根目录(相对路径)
pluginJsPath: lib/ # 脚本文件相对与插件根目录路径
pluginModelPath: assets/ # 模型文件相对与插件根目录路径
tagMode: false # 标签模式, 是否仅替换 live2d tag标签而非插入到所有页面中
debug: false # 调试, 是否在控制台输出日志
model:
use: remu # 填写放进live2d_models文件夹中的模型文件夹名字
scale: 1 # canvas 模型与canvas的缩放
display:
width: 150 # 宽度
height: 300 # 高度
position: left # 显示位置
hOffset: 0 #水平偏移
vOffset: -12 #垂直偏移
mobile:
show: true # 手机端是否显示
scale: 1 # 移动设备上的缩放
react:
opacity: 1 # 透明度
PS:按需加载live2d标签
上述配置中tagMode: true
则代表需要在主题中单独给出需要显示的页面
最终live2d效果
配置三方插件模型
上述步骤是下载remu源文件然后自定义的remu模型,也可以下载社区封装好npm插件的模型,本文做自定义模型的说明,故不做该部分详述。相关参考如下:
附:给live2d添加看板功能
上述所有步骤完成live2d的安装配置,在此基础上还可以给live2d添加看板功能,就如本文第一张图片那种效果
添加看板样式
添加静态样式即可:
_live2d/remu.styl
// 收缩之后的左下角的贴附样式
#waifu-toggle {
background-color: #3f51b5;
border-radius: 5px;
bottom: 66px;
color: #fff;
cursor: pointer;
font-size: 14px;
left: 5px;
margin-left: -90px;
padding: 5px 2px 5px 5px;
position: fixed;
transition: margin-left 1s;
width: 60px;
writing-mode: vertical-rl;
}
#waifu-toggle.waifu-toggle-active {
margin-left: -40px;
}
#waifu-toggle.waifu-toggle-active:hover {
margin-left: -30px;
}
// 容纳雷姆的盒子
#waifu {
bottom: -1000px;
left: 0;
line-height: 0;
margin-bottom: -10px;
position: fixed;
transform: translateY(3px);
transition: transform .3s ease-in-out, bottom 3s ease-in-out;
z-index: 1;
}
#waifu:hover {
transform: translateY(0);
}
// 文字提示框
#waifu-tips {
animation: shake 50s ease-in-out 5s infinite;
background-color: rgba(100,149,237, .5);
border: 1px solid rgba(30,144,255, .62);
border-radius: 12px;
box-shadow: 0 3px 15px 2px rgba(191, 158, 118, .2);
font-size: 14px;
line-height: 24px;
margin: -30px 20px;
min-height: 70px;
opacity: 0;
overflow: hidden;
padding: 5px 10px;
position: absolute;
text-overflow: ellipsis;
transition: opacity 1s;
width: 180px;
word-break: break-all;
}
#waifu-tips.waifu-tips-active {
opacity: 1;
transition: opacity .2s;
}
#waifu-tips span
color: rgba(0,0,255,.7); // 文字提示框中的重点字
// 容纳雷姆的外加小盒子
#waifu #live2d {
cursor: grab;
height: 300px;
position: relative;
width: 200px;
}
#waifu #live2d:active {
cursor: grabbing;
}
// 小按钮组件
#waifu-tool {
color: #0684bd;
opacity: 0.5;
position: absolute;
left: 5px
top: 70px;
transition: opacity 1s;
}
#waifu:hover #waifu-tool {
opacity: 1;
}
// 小按钮的span样式
#waifu-tool span {
color: #0684bd;
cursor: pointer;
display: block;
line-height: 30px;
text-align: center;
transition: color .3s;
}
#waifu-tool span:hover {
color: #0684bd; /* #34495e */
}
@keyframes shake {
2% {
transform: translate(.5px, -1.5px) rotate(-.5deg);
}
4% {
transform: translate(.5px, 1.5px) rotate(1.5deg);
}
6% {
transform: translate(1.5px, 1.5px) rotate(1.5deg);
}
8% {
transform: translate(2.5px, 1.5px) rotate(.5deg);
}
10% {
transform: translate(.5px, 2.5px) rotate(.5deg);
}
12% {
transform: translate(1.5px, 1.5px) rotate(.5deg);
}
14% {
transform: translate(.5px, .5px) rotate(.5deg);
}
16% {
transform: translate(-1.5px, -.5px) rotate(1.5deg);
}
18% {
transform: translate(.5px, .5px) rotate(1.5deg);
}
20% {
transform: translate(2.5px, 2.5px) rotate(1.5deg);
}
22% {
transform: translate(.5px, -1.5px) rotate(1.5deg);
}
24% {
transform: translate(-1.5px, 1.5px) rotate(-.5deg);
}
26% {
transform: translate(1.5px, .5px) rotate(1.5deg);
}
28% {
transform: translate(-.5px, -.5px) rotate(-.5deg);
}
30% {
transform: translate(1.5px, -.5px) rotate(-.5deg);
}
32% {
transform: translate(2.5px, -1.5px) rotate(1.5deg);
}
34% {
transform: translate(2.5px, 2.5px) rotate(-.5deg);
}
36% {
transform: translate(.5px, -1.5px) rotate(.5deg);
}
38% {
transform: translate(2.5px, -.5px) rotate(-.5deg);
}
40% {
transform: translate(-.5px, 2.5px) rotate(.5deg);
}
42% {
transform: translate(-1.5px, 2.5px) rotate(.5deg);
}
44% {
transform: translate(-1.5px, 1.5px) rotate(.5deg);
}
46% {
transform: translate(1.5px, -.5px) rotate(-.5deg);
}
48% {
transform: translate(2.5px, -.5px) rotate(.5deg);
}
50% {
transform: translate(-1.5px, 1.5px) rotate(.5deg);
}
52% {
transform: translate(-.5px, 1.5px) rotate(.5deg);
}
54% {
transform: translate(-1.5px, 1.5px) rotate(.5deg);
}
56% {
transform: translate(.5px, 2.5px) rotate(1.5deg);
}
58% {
transform: translate(2.5px, 2.5px) rotate(.5deg);
}
60% {
transform: translate(2.5px, -1.5px) rotate(1.5deg);
}
62% {
transform: translate(-1.5px, .5px) rotate(1.5deg);
}
64% {
transform: translate(-1.5px, 1.5px) rotate(1.5deg);
}
66% {
transform: translate(.5px, 2.5px) rotate(1.5deg);
}
68% {
transform: translate(2.5px, -1.5px) rotate(1.5deg);
}
70% {
transform: translate(2.5px, 2.5px) rotate(.5deg);
}
72% {
transform: translate(-.5px, -1.5px) rotate(1.5deg);
}
74% {
transform: translate(-1.5px, 2.5px) rotate(1.5deg);
}
76% {
transform: translate(-1.5px, 2.5px) rotate(1.5deg);
}
78% {
transform: translate(-1.5px, 2.5px) rotate(.5deg);
}
80% {
transform: translate(-1.5px, .5px) rotate(-.5deg);
}
82% {
transform: translate(-1.5px, .5px) rotate(-.5deg);
}
84% {
transform: translate(-.5px, .5px) rotate(1.5deg);
}
86% {
transform: translate(2.5px, 1.5px) rotate(.5deg);
}
88% {
transform: translate(-1.5px, .5px) rotate(1.5deg);
}
90% {
transform: translate(-1.5px, -.5px) rotate(-.5deg);
}
92% {
transform: translate(-1.5px, -1.5px) rotate(1.5deg);
}
94% {
transform: translate(.5px, .5px) rotate(-.5deg);
}
96% {
transform: translate(2.5px, -.5px) rotate(-.5deg);
}
98% {
transform: translate(-1.5px, -1.5px) rotate(-.5deg);
}
0%, 100% {
transform: translate(0, 0) rotate(0);
}
}
添加看板逻辑代码
./part/live2d.js
function loadWidget(config) {
let { waifuPath, apiPath, cdnPath } = config;
let useCDN = false, modelList;
if (typeof cdnPath === "string") {
useCDN = true;
if (!cdnPath.endsWith("/")) cdnPath += "/";
} else if (typeof apiPath === "string") {
if (!apiPath.endsWith("/")) apiPath += "/";
} else {
console.error("Invalid initWidget argument!");
return;
}
localStorage.removeItem("waifu-display");
sessionStorage.removeItem("waifu-text");
document.body.insertAdjacentHTML("beforeend", `<div id="waifu">
<div id="waifu-tips"></div>
<canvas id="live2d"></canvas>
<div id="waifu-tool">
<span class="fa fa-comments"></span>
<span class="fa fa-link"></span>
<span class="fa fa-compress"></span>
</div>
</div>`);
// https://stackoverflow.com/questions/24148403/trigger-css-transition-on-appended-element
setTimeout(() => {
document.getElementById("waifu").style.bottom = 0;
}, 0);
function randomSelection(obj) {
return Array.isArray(obj) ? obj[Math.floor(Math.random() * obj.length)] : obj;
}
// 检测用户活动状态,并在空闲时显示消息
let userAction = false,
userActionTimer,
messageTimer,
messageArray = ["好久不见,日子过得好快呢……", "嗨~欢迎访问该站点!", "记得把小家加入 Adblock 白名单哦!"];
window.addEventListener("mousemove", () => userAction = true);
window.addEventListener("keydown", () => userAction = true);
setInterval(() => {
if (userAction) {
userAction = false;
clearInterval(userActionTimer);
userActionTimer = null;
} else if (!userActionTimer) {
userActionTimer = setInterval(() => {
showMessage(randomSelection(messageArray), 6000, 9);
}, 20000);
}
}, 1000);
(function registerEventListener() {
// TODO: 第一个icon,说话
document.querySelector("#waifu-tool .fa-comments").addEventListener("click", showHitokoto);
// TODO: 第二个icon,貌似是什么游戏
// document.querySelector("#waifu-tool .fa-paper-plane").addEventListener("click", () => {
// if (window.Asteroids) {
// if (!window.ASTEROIDSPLAYERS) window.ASTEROIDSPLAYERS = [];
// window.ASTEROIDSPLAYERS.push(new Asteroids());
// } else {
// const script = document.createElement("script");
// script.src = "https://cdn.jsdelivr.net/gh/stevenjoezhang/asteroids/asteroids.js";
// document.head.appendChild(script);
// }
// });
// TODO: 第三个icon,换人物
// document.querySelector("#waifu-tool .fa-user-circle").addEventListener("click", loadOtherModel);
// TODO: 第四个icon,应该是换装
// document.querySelector("#waifu-tool .fa-street-view").addEventListener("click", loadRandModel);
// TODO: 第五个icon,拍照
// document.querySelector("#waifu-tool .fa-camera-retro").addEventListener("click", () => {
// showMessage("照好了嘛,是不是很可爱呢?", 6000, 9);
// Live2D.captureName = "photo.png";
// Live2D.captureFrame = true;
// });
// TODO: 第六个icon,原项目地址
document.querySelector("#waifu-tool .fa-link").addEventListener("click", () => {
open("https://www.wztlink1013.com/about/");
});
// TODO: 第七个icon,隐藏看板娘
document.querySelector("#waifu-tool .fa-compress").addEventListener("click", () => {
localStorage.setItem("waifu-display", Date.now());
showMessage("愿你有一天能与重要的人重逢。", 2000, 11);
document.getElementById("waifu").style.bottom = "-500px"; // 隐藏大盒子
setTimeout(() => {
document.getElementById("waifu").style.display = "none";
document.getElementById("waifu-toggle").classList.add("waifu-toggle-active");
}, 3000);
});
window.addEventListener("copy", () => {
showMessage("你都复制了些什么呀,转载要记得加上出处哦!", 6000, 9);
});
window.addEventListener("visibilitychange", () => {
if (!document.hidden) showMessage("哇,你终于回来了~", 6000, 9);
});
})();
// 首页展示特定文字
(function welcomeMessage() {
let text;
if (location.pathname === "/") { // 如果是主页
const now = new Date().getHours();
if (now > 5 && now <= 7) text = "早上好!一日之计在于晨,美好的一天就要开始了。";
else if (now > 7 && now <= 11) text = "上午好!工作顺利嘛,不要久坐,多起来走动走动哦!";
else if (now > 11 && now <= 13) text = "中午了,工作了一个上午,现在是午餐时间!";
else if (now > 13 && now <= 17) text = "午后很容易犯困呢,今天的运动目标完成了吗?";
else if (now > 17 && now <= 19) text = "傍晚了!窗外夕阳的景色很美丽呢,最美不过夕阳红~";
else if (now > 19 && now <= 21) text = "晚上好,今天过得怎么样?";
else if (now > 21 && now <= 23) text = ["已经这么晚了呀,早点休息吧,晚安~", "深夜时要爱护眼睛呀!"];
else text = "你是夜猫子呀?这么晚还不睡觉,明天起的来嘛?";
} else if (document.referrer !== "") {
const referrer = new URL(document.referrer),
domain = referrer.hostname.split(".")[1];
if (location.hostname === referrer.hostname) text = `欢迎阅读<span>「${document.title.split(" - ")[0]}」</span>`;
else if (domain === "baidu") text = `Hello!来自 百度搜索 的朋友<br>你是搜索 <span>${referrer.search.split("&wd=")[1].split("&")[0]}</span> 找到的我吗?`;
else if (domain === "so") text = `Hello!来自 360搜索 的朋友<br>你是搜索 <span>${referrer.search.split("&q=")[1].split("&")[0]}</span> 找到的我吗?`;
else if (domain === "google") text = `Hello!来自 谷歌搜索 的朋友<br>欢迎阅读<span>「${document.title.split(" - ")[0]}」</span>`;
else text = `Hello!来自 <span>${referrer.hostname}</span> 的朋友`;
} else {
text = `欢迎阅读<span>「${document.title.split(" - ")[0]}」</span>`;
}
showMessage(text, 7000, 8);
})();
// TODO: 服务于第一个icon
function showHitokoto() {
// 增加 hitokoto.cn 的 API
fetch("https://v1.hitokoto.cn")
.then(response => response.json())
.then(result => {
const text = `这句一言来自 <span>「${result.from}」</span>,是 <span>${result.creator}</span> 在 hitokoto.cn 投稿的。`;
showMessage(result.hitokoto, 6000, 9);
setTimeout(() => {
showMessage(text, 4000, 9);
}, 6000);
});
}
// 没有bug展示特定状态下的文字
function showMessage(text, timeout, priority) {
if (!text || (sessionStorage.getItem("waifu-text") && sessionStorage.getItem("waifu-text") > priority)) return;
if (messageTimer) {
clearTimeout(messageTimer);
messageTimer = null;
}
text = randomSelection(text);
sessionStorage.setItem("waifu-text", priority);
const tips = document.getElementById("waifu-tips");
tips.innerHTML = text;
tips.classList.add("waifu-tips-active");
messageTimer = setTimeout(() => {
sessionStorage.removeItem("waifu-text");
tips.classList.remove("waifu-tips-active");
}, timeout);
}
}
function initWidget(config, apiPath) {
if (typeof config === "string") config = { waifuPath: config, apiPath };
document.body.insertAdjacentHTML("beforeend", `<div id="waifu-toggle">
<span>雷姆</span>
</div>`);
const toggle = document.getElementById("waifu-toggle");
toggle.addEventListener("click", () => {
toggle.classList.remove("waifu-toggle-active");
if (toggle.getAttribute("first-time")) {
loadWidget(config);
toggle.removeAttribute("first-time");
} else {
localStorage.removeItem("waifu-display");
document.getElementById("waifu").style.display = "";
setTimeout(() => {
document.getElementById("waifu").style.bottom = 0;
}, 0);
}
// TODO: 一开始判断是否隐藏雷姆代码
});
if (localStorage.getItem("waifu-display") && Date.now() - localStorage.getItem("waifu-display") <= 86400000) {
toggle.setAttribute("first-time", true);
setTimeout(() => {
toggle.classList.add("waifu-toggle-active");
}, 0);
} else {
loadWidget(config);
}
}
export {
initWidget, //导出对象
}
在base.js中初始化看板逻辑代码
import {initWidget} from './part/live2d.js';
// live2d 初始化
initWidget({
waifuPath: "/data/remu.json",
cdnPath: "https://github.com/fghrsh/live2d_api/tree/1.0.1/"
});
最终看板效果
参考
- live2d API文档:https://l2dwidget.js.org/docs/class/src/index.js~L2Dwidget.html#instance-method-init
- 封装为npm插件模型:https://huaji8.top/post/live2d-plugin-2.0/
- 自定义扩展模型:https://github.com/luanshizhimei/live2d_models_collect
- vscode live2d扩展:https://github.com/ezshine/vscode-rainbow-fart-waifu
- https://github.com/fghrsh/live2d_api/tree/1.0.1/
- TODO: 优化资源加载方式,模型的静态资源使用外链的形式较为合理,以及插件每次在开发模式下都会编译文件,都会带来开发环境以及生产环境上的不便
评论区