在博客中添加明日方舟小人

编辑文章

需要使用的游戏内资源文件请自行获取。

配置

在 Hexo 的 _config.yaml 中添加如下内容(部分设置需要自己更改)。

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
arknights_live2d:
enable: true # 是否启用
urlPrefix: "/live2d/char_1012_skadi2/" # 所有url的前缀
styles:
widget: # 展示窗口的样式,可以自行添加需要设置的CSS属性
width: "200px"
height: "200px"
voiceText: # 语音文字的样式,可以自行添加需要设置的CSS属性
color: "#e6e6e6"
skin: "default" # 使用的皮肤
atlas: "char_1012_skadi2.atlas" # atlas文件的url
skeleton: "char_1012_skadi2.skel" # skeleton文件的url
behaviors:
start: # 人物刚出现时触发的行为
animation: "Start" # 动画名
voice: "audios/CN_042.mp3" # 音频的url
text: "博士......来这里。" # 语音文本
idle: # 人物被闲置时触发的行为
animation: "Idle" # 动画名
voice: "audios/CN_010.mp3" # 音频的url
text: "梦......你会有怎样一个梦呢?你现在身处何方?那里......会不会也是一个梦呢?" # 语音文本
maxMinutes: 5 # 最大闲置时间(分钟)
interact: # 人物被点击时触发的行为
animations:
- name: "Skill_2_Begin" # 动画名
loop: false # 语音结束前,是否循环播放
- name: "Skill_2_Loop" # 动画名
loop: true # 语音结束前,是否循环播放
voices:
- voice: "audios/CN_002.mp3" # 音频的url
text: "你以前觉得我们彻底离开海水就没法生存?不是的。我和我的血亲们就快能在空气里游弋了。到了最后,哪怕是那些什么都没有的地方,也能成为我们的海洋。我们只需要进化就好,不断地进化。" # 语音文本

注入器

使用 Hexo Injector 将初始化代码注入至 body 标签后。

JavaScript
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
hexo.extend.injector.register('body_end', function () {
const {
enable,
urlPrefix,
styles,
skin,
atlas,
skeleton,
behaviors
} = hexo.config.arknights_live2d;

if (!enable) {
return null;
}

return `
<div class="arknights-spine-widget"></div>
<script src="/js/spine-widget.js"></script>
<script src="/js/spine-skeleton-binary3.5.51.js"></script>
<script src="/js/arknights_live2d.js"></script>
<link rel="stylesheet" href="/css/arknights_live2d.css"></link>
<script>
new ArknightsLive2D({
urlPrefix: "${urlPrefix}",
styles: ${JSON.stringify(styles)},
skin: "${skin}",
atlas: "${atlas}",
skeleton: "${skeleton}",
behaviors: ${JSON.stringify(behaviors)}
});
</script>
`
});

Spine 库文件

提示:明日方舟使用的 Spine 版本为 3.5.51。

spine-widget.js

下载地址

spine-skeleton-binary3.5.51.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
40
41
42
43
44
.arknights-spine-widget {
width: 200px;
height: 200px;
position: fixed;
left: 0;
bottom: 0;
z-index: 999;
}

.arknights-voice-text {
width: 100%;
height: 60px;
position: absolute;
margin-top: -70px;
padding: 5px 10px;
color: #fff;
background-color: #14151685;
box-shadow: 0 3px 15px 2px #141516;
font-size: xx-small;
text-align: left;
text-overflow: ellipsis;
overflow-x: hidden;
overflow-y: scroll;
transition: opacity 300ms ease-in-out, height 0.5s ease-in-out;
}

.arknights-voice-text::before {
content: 'VOICE TEXT';
display: block;
color: #37b2ff;
font-weight: bold;
}

/* hide the scroll bar */

.arknights-voice-text {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
}

.arknights-voice-text::-webkit-scrollbar {
display: none; /* Chrome Safari */
}

Spine 控制代码

JavaScript
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
function ArknightsLive2D(config) {
this.config = config;
this.widget = null;

this.widgetContainer = document.querySelector(".arknights-spine-widget");
this.voiceText = document.createElement("div");
this.voicePlayer = new Audio();

this.triggerEvents = ["mousedown", "touchstart", "scroll"];
this.animationQueue = new Array(); // 动画播放队列
this.isPlayingVoice = false;
this.lastInteractTime = Date.now();
this.localX = 0;
this.localY = 0;

this.load();
}

ArknightsLive2D.downloadBinary = function (url, success, error) {
var request = new XMLHttpRequest();
request.open("GET", url, true);
request.responseType = "arraybuffer";
request.onload = function () {
if (request.status == 200) {
success(new Uint8Array(request.response));
} else {
error(request.status, request.responseText);
}
};
request.onerror = function () {
error(request.status, request.responseText);
};
request.send();
};

ArknightsLive2D.prototype = {
load: function () {
let c = this.config;

ArknightsLive2D.downloadBinary(this.getUrl(c.skeleton), data => {
function setStyle(element, style) {
for (var prop in style) {
element.style.setProperty(prop, style[prop]);
}
}

setStyle(this.widgetContainer, c.styles.widget);
setStyle(this.voiceText, c.styles.voiceText);

var skeletonJson = new spine.SkeletonJsonConverter(data, 1);
skeletonJson.convertToJson();

new spine.SpineWidget(this.widgetContainer, {
animation: this.getAnimationList("start")[0].name,
skin: c.skin,
atlas: this.getUrl(c.atlas),
jsonContent: skeletonJson.json,
backgroundColor: "#00000000",
loop: false,
success: this.spineWidgetSuccessCallback.bind(this)
});
}, function (status, responseText) {
console.error(`Couldn't download skeleton ${path}: status ${status}, ${responseText}.`);
});
},

spineWidgetSuccessCallback: function (widget) {
var init = () => {
this.triggerEvents.forEach(e => window.removeEventListener(e, init));
this.triggerEvents.forEach(e => window.addEventListener(e, this.tryPlayingIdleVoice.bind(this)));

this.initVoiceComponents();
this.initWidgetActions();
this.initDragging();

this.widget.play(); // 开始播放动画
this.playVoice(this.getVoice("start"));
this.widgetContainer.style.display = "block";
};

this.widget = widget;
this.widget.pause(); // 停止动画播放
this.widgetContainer.style.display = "none"; // 隐藏
this.triggerEvents.forEach(e => window.addEventListener(e, init));
},

initVoiceComponents: function () {
this.voiceText.setAttribute("class", "arknights-voice-text");
this.widgetContainer.appendChild(this.voiceText); // 保证在canvas之上
this.voiceText.style.opacity = 0; // 默认隐藏

// 自动滚动文字
this.voicePlayer.addEventListener("timeupdate", () => {
this.voiceText.scrollTo({
left: 0,
top: this.voiceText.offsetHeight * (this.voicePlayer.currentTime / this.voicePlayer.duration),
behavior: "smooth"
});
});

this.voicePlayer.addEventListener("ended", () => {
this.voiceText.style.opacity = 0; // 播放完立刻隐藏
this.isPlayingVoice = false;
});
},

initWidgetActions: function () {
this.widget.canvas.onclick = this.interact.bind(this);
this.widget.state.addListener({
complete: entry => {
// 如果音频没播放完就一直循环指定的动画,而不是回到闲置动画
if (this.isPlayingVoice && entry.loop) {
this.playAllAnimations({
name: entry.animation.name,
loop: true
});
} else {
this.playAllAnimations(this.animationQueue.shift() || this.getAnimationList("idle"));
}
}
});
},

initDragging: function () {
function getPagePos(event) {
var x = document.documentElement.scrollLeft;
var y = document.documentElement.scrollTop;

if (event.targetTouches) {
x += event.targetTouches[0].clientX;
y += event.targetTouches[0].clientY;
} else if (event.clientX && event.clientY) {
x += event.clientX;
y += event.clientY;
}

return {
x: x,
y: y
};
}

function preventDefault(event) {
if (event.cancelable) {
event.preventDefault();
}
}

var setWidgetPos = (left, top) => {
left = Math.max(0, left);
top = Math.max(0, top);
left = Math.min(document.body.clientWidth - this.widgetContainer.clientWidth, left);
top = Math.min(document.body.clientHeight - this.widgetContainer.clientHeight, top);

this.widgetContainer.style.left = left + "px";
this.widgetContainer.style.top = top + "px";
};

var down = event => {
var {
x,
y
} = getPagePos(event);

this.localX = x - this.widgetContainer.offsetLeft;
this.localY = y - this.widgetContainer.offsetTop;
};

var move = event => {
var {
x,
y
} = getPagePos(event);

setWidgetPos(x - this.localX, y - this.localY);
window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty(); // 清除选中文字
};

var passive = {
passive: true
};
var nonPassive = {
passive: false
};

this.widgetContainer.addEventListener("mousedown", event => {
down(event);
document.addEventListener("mousemove", move); // 防止鼠标快速滑出
});
this.widgetContainer.addEventListener('touchstart', event => {
down(event);
document.addEventListener("touchmove", preventDefault, nonPassive); // 防止屏幕滚动
}, passive)

this.widgetContainer.addEventListener('touchmove', move, passive)

document.addEventListener("mouseup", () => document.removeEventListener("mousemove", move));
this.widgetContainer.addEventListener('touchend', () => document.removeEventListener("touchmove", preventDefault));

window.addEventListener("resize", () => {
let style = this.widgetContainer.style;

if (style.left && style.top) {
var left = Number.parseInt(style.left.substring(0, style.left.length - 2));
var top = Number.parseInt(style.top.substring(0, style.top.length - 2));
setWidgetPos(left, top); // 防止窗口大小变化时人物消失
}
});
},

interact: function () {
if (this.isPlayingVoice || this.animationQueue.length > 0 || !this.isIdle()) {
console.warn("互动过于频繁!");
} else {
this.lastInteractTime = Date.now();
this.playAllAnimations(this.getAnimationList("interact"));
this.playVoice(this.getVoice("interact"));
}
},

getUrl: function (file) {
return this.config.urlPrefix + file;
},

getAnimationList: function (behaviorName) {
var behavior = this.config.behaviors[behaviorName];
if (behaviorName == "start" || behaviorName == "idle") {
return [{
name: behavior.animation,
loop: false
}];
}
return behavior.animations.slice(); // 拷贝一份,防止外部修改
},

getVoice: function (behaviorName) {
var behavior = this.config.behaviors[behaviorName];
if (behaviorName == "start" || behaviorName == "idle") {
return {
voice: behavior.voice,
text: behavior.text
};
}
return behavior.voices[Math.floor(Math.random() * behavior.voices.length)];
},

playAllAnimations: function (animations) {
if (Array.isArray(animations)) {
this.playAllAnimations(animations.shift());
animations.forEach(a => this.animationQueue.push(a)); // 加入播放队列
} else if (animations) {
// this.widget.setAnimation 会先重置人物的姿势,让动画切换不连贯
this.widget.state.setAnimation(0, animations.name, animations.loop);
}
},

playVoice: function (voice) {
if (voice) {
this.isPlayingVoice = true;
this.voicePlayer.src = this.getUrl(voice.voice);
this.voicePlayer.load();
this.voicePlayer.play().then(() => {
this.voiceText.innerHTML = voice.text;
this.voiceText.scrollTo(0, 0); // 立刻滑动至最上方
this.voiceText.style.opacity = 1;
}, reason => {
this.isPlayingVoice = false;
console.error(`无法播放音频,因为:${reason}`);
});

}
},

isIdle: function () {
return this.widget.state.tracks[0].animation.name == this.getAnimationList("idle")[0].name;
},

tryPlayingIdleVoice: function () {
var time = Date.now();
var delta = time - this.lastInteractTime;
var hour = Math.floor(delta / 1000 / 60 / 60);
var minute = Math.floor(delta / 1000 / 60 - hour * 60);

if (minute >= this.config.behaviors.idle.maxMinutes) {
this.lastInteractTime = time;
this.playVoice(this.getVoice("idle"));
}
}
};

在博客中添加明日方舟小人

链接 https://jin-yuhan.github.io/posts/4eb44f55/

作者

Stalo

发布于

2021年5月4日 晚上

更新于

2021年12月12日 下午

许可协议


赞赏

感谢您的赞赏☆(≧∀≦*)ノ

微信赞赏码

微信