Skip to content

Commit f246a2c

Browse files
committed
Split live2d script. Improve live2d pointer event logic.
1 parent e16a62f commit f246a2c

10 files changed

Lines changed: 3145 additions & 2932 deletions

File tree

static/live2d-core.js

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* Live2D Core - 核心类结构和基础功能
3+
*/
4+
5+
window.PIXI = PIXI;
6+
const {Live2DModel} = PIXI.live2d;
7+
8+
// 全局变量
9+
let currentModel = null;
10+
let emotionMapping = null;
11+
let currentEmotion = 'neutral';
12+
let pixi_app = null;
13+
let isInitialized = false;
14+
15+
let motionTimer = null; // 动作持续时间定时器
16+
let isEmotionChanging = false; // 防止快速连续点击的标志
17+
18+
// 全局:判断是否为移动端宽度
19+
const isMobileWidth = () => window.innerWidth <= 768;
20+
21+
// Live2D 管理器类
22+
class Live2DManager {
23+
constructor() {
24+
this.currentModel = null;
25+
this.emotionMapping = null; // { motions: {emotion: [string]}, expressions: {emotion: [string]} }
26+
this.fileReferences = null; // 保存原始 FileReferences(含 Motions/Expressions)
27+
this.currentEmotion = 'neutral';
28+
this.pixi_app = null;
29+
this.isInitialized = false;
30+
this.motionTimer = null;
31+
this.isEmotionChanging = false;
32+
this.dragEnabled = false;
33+
this.isFocusing = false;
34+
this.isLocked = true;
35+
this.onModelLoaded = null;
36+
this.onStatusUpdate = null;
37+
this.modelName = null; // 记录当前模型目录名
38+
this.modelRootPath = null; // 记录当前模型根路径,如 /static/<modelName>
39+
40+
// 常驻表情:使用官方 expression 播放并在清理后自动重放
41+
this.persistentExpressionNames = [];
42+
43+
// UI/Ticker 资源句柄(便于在切换模型时清理)
44+
this._lockIconTicker = null;
45+
this._lockIconElement = null;
46+
47+
// 浮动按钮系统
48+
this._floatingButtonsTicker = null;
49+
this._floatingButtonsContainer = null;
50+
this._floatingButtons = {}; // 存储所有按钮元素
51+
this._popupTimers = {}; // 存储弹出框的定时器
52+
this._goodbyeClicked = false; // 标记是否点击了"请她离开"
53+
54+
// 已打开的设置窗口引用映射(URL -> Window对象)
55+
this._openSettingsWindows = {};
56+
57+
// 口型同步控制
58+
this.mouthValue = 0; // 0~1
59+
this.mouthParameterId = null; // 例如 'ParamMouthOpenY' 或 'ParamO'
60+
this._mouthOverrideInstalled = false;
61+
this._origUpdateParameters = null;
62+
this._origExpressionUpdateParameters = null;
63+
this._mouthTicker = null;
64+
}
65+
66+
// 从 FileReferences 推导 EmotionMapping(用于兼容历史数据)
67+
deriveEmotionMappingFromFileRefs(fileRefs) {
68+
const result = { motions: {}, expressions: {} };
69+
70+
try {
71+
// 推导 motions
72+
const motions = (fileRefs && fileRefs.Motions) || {};
73+
Object.keys(motions).forEach(group => {
74+
const items = motions[group] || [];
75+
const files = items
76+
.map(item => (item && item.File) ? String(item.File) : null)
77+
.filter(Boolean);
78+
result.motions[group] = files;
79+
});
80+
81+
// 推导 expressions(按 Name 前缀分组)
82+
const expressions = (fileRefs && Array.isArray(fileRefs.Expressions)) ? fileRefs.Expressions : [];
83+
expressions.forEach(item => {
84+
if (!item || typeof item !== 'object') return;
85+
const name = String(item.Name || '');
86+
const file = String(item.File || '');
87+
if (!file) return;
88+
const group = name.includes('_') ? name.split('_', 1)[0] : 'neutral';
89+
if (!result.expressions[group]) result.expressions[group] = [];
90+
result.expressions[group].push(file);
91+
});
92+
} catch (e) {
93+
console.warn('从 FileReferences 推导 EmotionMapping 失败:', e);
94+
}
95+
96+
return result;
97+
}
98+
99+
// 初始化 PIXI 应用
100+
async initPIXI(canvasId, containerId, options = {}) {
101+
if (this.isInitialized) {
102+
console.warn('Live2D 管理器已经初始化');
103+
return this.pixi_app;
104+
}
105+
106+
const defaultOptions = {
107+
autoStart: true,
108+
transparent: true,
109+
backgroundAlpha: 0
110+
};
111+
112+
this.pixi_app = new PIXI.Application({
113+
view: document.getElementById(canvasId),
114+
resizeTo: document.getElementById(containerId),
115+
...defaultOptions,
116+
...options
117+
});
118+
119+
this.isInitialized = true;
120+
return this.pixi_app;
121+
}
122+
123+
// 加载用户偏好
124+
async loadUserPreferences() {
125+
try {
126+
const response = await fetch('/api/preferences');
127+
if (response.ok) {
128+
return await response.json();
129+
}
130+
} catch (error) {
131+
console.warn('加载用户偏好失败:', error);
132+
}
133+
return [];
134+
}
135+
136+
// 保存用户偏好
137+
async saveUserPreferences(modelPath, position, scale) {
138+
try {
139+
const preferences = {
140+
model_path: modelPath,
141+
position: position,
142+
scale: scale
143+
};
144+
const response = await fetch('/api/preferences', {
145+
method: 'POST',
146+
headers: {
147+
'Content-Type': 'application/json',
148+
},
149+
body: JSON.stringify(preferences)
150+
});
151+
const result = await response.json();
152+
return result.success;
153+
} catch (error) {
154+
console.error("保存偏好失败:", error);
155+
return false;
156+
}
157+
}
158+
159+
// 随机选择数组中的一个元素
160+
getRandomElement(array) {
161+
if (!array || array.length === 0) return null;
162+
return array[Math.floor(Math.random() * array.length)];
163+
}
164+
165+
// 解析资源相对路径(基于当前模型根目录)
166+
resolveAssetPath(relativePath) {
167+
if (!relativePath) return '';
168+
let rel = String(relativePath).replace(/^[\\/]+/, '');
169+
if (rel.startsWith('static/')) {
170+
return `/${rel}`;
171+
}
172+
if (rel.startsWith('/static/')) {
173+
return rel;
174+
}
175+
return `${this.modelRootPath}/${rel}`;
176+
}
177+
178+
// 获取当前模型
179+
getCurrentModel() {
180+
return this.currentModel;
181+
}
182+
183+
// 获取当前情感映射
184+
getEmotionMapping() {
185+
return this.emotionMapping;
186+
}
187+
188+
// 获取 PIXI 应用
189+
getPIXIApp() {
190+
return this.pixi_app;
191+
}
192+
}
193+
194+
// 导出
195+
window.Live2DModel = Live2DModel;
196+
window.Live2DManager = Live2DManager;
197+
window.isMobileWidth = isMobileWidth;
198+

0 commit comments

Comments
 (0)