前言:为自己站点配置雷姆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: 优化资源加载方式,模型的静态资源使用外链的形式较为合理,以及插件每次在开发模式下都会编译文件,都会带来开发环境以及生产环境上的不便
评论区