Files
tools/Spine插件/Spine_动画播放器_2.0/logic_4.2.js
T

411 lines
15 KiB
JavaScript

// Spine 4.2 逻辑适配器 (基于 V30 + 全局配置 + Timeline + DebugDraw)
window.startSpine42 = async function(canvas, files) {
console.log("启动 Spine 4.2 引擎...");
// =============================================================
// 动态增强 UI
// =============================================================
const uiPanel = document.getElementById('ui');
// 适配新版 UI:绑定现有的 PMA checkbox
const chkPma = document.getElementById('chk-pma');
if (chkPma) {
chkPma.onchange = (e) => {
// 4.2 可能需要特定的处理,或者只是重绘
// 这里假设 render loop 会读取 global pma setting
window.viewerConfig.pmaEnabled = e.target.checked;
};
}
// 物理重置按钮保留,但样式微调
if (!document.getElementById('btn-reset-phy')) {
const btn = document.createElement('button');
btn.id = 'btn-reset-phy';
btn.innerText = "💥 重置物理";
btn.className = 'btn-export'; // 复用导出按钮的样式
btn.style.marginTop = '15px';
btn.style.background = '#880000'; // 特殊颜色
btn.onclick = () => resetPhysics();
uiPanel.appendChild(btn);
}
// =============================================================
// 核心变量
// =============================================================
const ui = {
log: (msg, type) => {
console.log(msg);
const box = document.getElementById('log');
if(box) box.innerText = msg;
},
updateSliders: () => {
const sx = document.getElementById('posX');
const sy = document.getElementById('posY');
if(sx) sx.value = cam.x;
if(sy) sy.value = cam.y;
}
};
const Mat4 = {
create: () => new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]),
ortho: (out, l, r, b, t, n, f) => {
const lr = 1/(l-r), bt = 1/(b-t), nf = 1/(n-f);
out[0]=-2*lr; out[5]=-2*bt; out[10]=2*nf; out[12]=(l+r)*lr; out[13]=(t+b)*bt; out[14]=(f+n)*nf; out[15]=1;
}
};
let gl, shader, batcher, renderer;
let skeleton, state, mvp = Mat4.create();
let lastTime = Date.now();
let cam = {x:0, y:0, zoom:1.0};
window.cam42 = cam; // 暴露给导出脚本使用
let globalTexture = null;
const Physics = { None: 0, Update: 1, Reset: 2 };
// =============================================================
// 初始化 WebGL
// =============================================================
try {
// 关键修复:添加 premultipliedAlpha: false,解决半透明黑边问题
gl = canvas.getContext('webgl', { alpha: true, preserveDrawingBuffer: true, premultipliedAlpha: false });
if(!gl) throw "WebGL 不可用";
// 确保 WebGL 不自动预乘 (因为 PMA 图片本身已经是预乘过的)
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
shader = spine.Shader.newTwoColoredTextured(gl);
batcher = new spine.PolygonBatcher(gl, true);
renderer = new spine.SkeletonRenderer(gl);
renderer.twoColor = true;
renderer.premultipliedAlpha = true;
bindInput();
// 开始处理文件
handleFiles(files);
// 启动循环
requestAnimationFrame(loop);
} catch(e) { ui.log("初始化失败: " + e, 'err'); }
// =============================================================
// 输入绑定
// =============================================================
function bindInput() {
// 鼠标/滚轮控制
canvas.onwheel = e => {
cam.zoom *= (e.deltaY>0 ? 0.9 : 1.1);
if(cam.zoom < 0.1) cam.zoom = 0.1;
if(cam.zoom > 10) cam.zoom = 10;
};
let drag=false, lx, ly, isRight=false;
canvas.oncontextmenu = e => e.preventDefault();
canvas.onmousedown = e => {
drag=true;
isRight = (e.button === 2);
lx=e.clientX; ly=e.clientY;
};
window.onmouseup = () => drag=false;
window.onmousemove = e => {
if(drag) {
let dx = e.clientX - lx;
let dy = e.clientY - ly;
if(isRight) {
let newZoom = cam.zoom * (1 + dy * 0.005);
if(newZoom > 0.1 && newZoom < 10) cam.zoom = newZoom;
} else {
cam.x -= dx / cam.zoom;
cam.y += dy / cam.zoom;
// 物理惯性
if (skeleton) {
skeleton.x += dx / cam.zoom;
skeleton.y -= dy / cam.zoom;
}
ui.updateSliders();
}
lx = e.clientX; ly = e.clientY;
}
};
const sx = document.getElementById('posX');
const sy = document.getElementById('posY');
if(sx) sx.oninput = () => cam.x = parseFloat(sx.value);
if(sy) sy.oninput = () => cam.y = parseFloat(sy.value);
// 绑定全局归位函数
window.resetView = function() {
if(skeleton) {
// 4.2 需要同时重置摄像机和骨骼位置(如果有物理惯性)
cam.x = 0;
cam.y = -200; // 4.2 默认稍微向下偏移一点
cam.zoom = 1.0;
// 重置滑块
if(sx) sx.value = 0;
if(sy) sy.value = -200;
ui.log("视图已重置");
}
};
}
function resetPhysics() {
if(skeleton && spine.Physics) {
skeleton.updateWorldTransform(spine.Physics.reset);
ui.log("物理已重置");
}
}
// =============================================================
// 文件处理
// =============================================================
async function handleFiles(fileList) {
let map = {};
for(let f of fileList) {
let n = f.name.toLowerCase();
if(n.includes('.atlas')) map.atlas = f;
else if(n.includes('.png') || n.includes('.jpg') || n.includes('.webp')) map.png = f;
else if(n.includes('.skel') || n.includes('.json')) map.main = f;
}
if(!map.atlas || !map.png || !map.main) {
ui.log("文件缺失: 需要 .json/.skel + .atlas + .png", 'err');
return;
}
try {
const imgBlob = URL.createObjectURL(map.png);
const img = new Image();
img.onload = () => processSpine(map, img);
img.src = imgBlob;
} catch(e) { ui.log(e, 'err'); }
}
async function processSpine(map, img) {
try {
ui.log("正在加载 (Spine 4.2)...");
const atlasText = await readFile(map.atlas, 'text');
globalTexture = new spine.GLTexture(gl, img, false);
globalTexture.setFilters(gl.LINEAR, gl.LINEAR);
globalTexture.setWraps(gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE);
const atlas = new spine.TextureAtlas(atlasText, path => globalTexture);
// 强制修正 Atlas 尺寸
for(let page of atlas.pages) { page.width = img.width; page.height = img.height; }
const loader = new spine.AtlasAttachmentLoader(atlas);
let skelData;
if(map.main.name.includes('.skel')) {
ui.log("解析二进制文件...");
const buffer = await readFile(map.main, 'array');
skelData = new spine.SkeletonBinary(loader).readSkeletonData(new Uint8Array(buffer));
} else {
ui.log("解析 JSON 文件...");
const text = await readFile(map.main, 'text');
skelData = new spine.SkeletonJson(loader).readSkeletonData(JSON.parse(text));
}
ui.log(`版本: ${skelData.version}`);
skeleton = new spine.Skeleton(skelData);
window.skeleton = skeleton; // 暴露给导出脚本
// 自动注入纹理
let fixCount = 0;
for(let skin of skeleton.data.skins) {
let entries = skin.getAttachments ? skin.getAttachments() : skin.attachments;
for(let entry of entries) {
let att = entry ? entry.attachment : null;
if(att && att.region) {
att.region.texture = globalTexture;
att.region.renderObject = att.region;
fixCount++;
}
}
}
skeleton.setToSetupPose();
if(spine.Physics) skeleton.updateWorldTransform(spine.Physics.reset);
state = new spine.AnimationState(new spine.AnimationStateData(skelData));
setupUI(skelData);
// 预热两帧
state.update(0); state.apply(skeleton); skeleton.updateWorldTransform(spine.Physics.update);
state.update(0); state.apply(skeleton); skeleton.updateWorldTransform(spine.Physics.update);
// 自动对焦
const offset = new spine.Vector2(), size = new spine.Vector2();
skeleton.getBounds(offset, size, []);
if(size.x > 0) {
cam.x = offset.x + size.x/2;
cam.y = offset.y + size.y/2;
cam.zoom = Math.min(canvas.width/size.x, canvas.height/size.y) * 0.8;
} else {
cam.x = 0; cam.y = -200;
}
ui.updateSliders();
// 显示UI
document.getElementById('ui').style.display = 'block';
document.getElementById('controls').style.display = 'flex';
} catch(e) { ui.log(e.message, 'err'); }
}
function setupUI(data) {
const animSelect = document.getElementById('anim-select');
const skinSelect = document.getElementById('skin-select');
if(!animSelect || !skinSelect) return;
animSelect.innerHTML = "";
skinSelect.innerHTML = "";
data.animations.forEach(a => {
const opt = document.createElement('option');
opt.value = a.name; opt.text = a.name; animSelect.appendChild(opt);
});
if(data.animations.length > 0) {
state.setAnimation(0, data.animations[0].name, true);
}
data.skins.forEach(s => {
const opt = document.createElement('option');
opt.value = s.name; opt.text = s.name; skinSelect.appendChild(opt);
});
if(data.defaultSkin) skinSelect.value = data.defaultSkin.name;
animSelect.onchange = () => state.setAnimation(0, animSelect.value, true);
skinSelect.onchange = () => {
skeleton.setSkinByName(skinSelect.value);
skeleton.setSlotsToSetupPose();
skeleton.setToSetupPose();
if(spine.Physics) skeleton.updateWorldTransform(spine.Physics.reset);
};
}
function readFile(f, type) {
return new Promise(r => {
const fr = new FileReader();
fr.onload = () => r(fr.result);
if(type==='text') fr.readAsText(f); else fr.readAsArrayBuffer(f);
});
}
// =============================================================
// 渲染循环
// =============================================================
function loop() {
requestAnimationFrame(loop);
const now = Date.now();
const targetFps = window.viewerConfig.targetFps || 30;
const interval = 1000 / targetFps;
const elapsed = now - lastTime;
if (elapsed < interval) return;
lastTime = now - (elapsed % interval);
let dt = elapsed / 1000;
// 应用倍速
const speed = window.viewerConfig.speed || 1.0;
dt *= speed;
// === 导出模式判断 ===
const isExporting = window.viewerConfig.isExporting;
if (!isExporting) {
if(canvas.width !== window.innerWidth) { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
}
gl.viewport(0, 0, canvas.width, canvas.height);
if (isExporting) {
gl.clearColor(0, 0, 0, 0); // 强制全透明
} else {
const bg = window.viewerConfig ? window.viewerConfig.bgColor : [0.2, 0.2, 0.2, 1];
gl.clearColor(bg[0], bg[1], bg[2], bg[3]);
}
gl.clear(gl.COLOR_BUFFER_BIT);
if(!skeleton || !state) return;
try {
// === 时间轴核心控制 ===
const ctrl = window.animControl;
let track = state.getCurrent(0);
if (track) {
if (ctrl) ctrl.updateUI(track.trackTime, track.animation.duration);
if (ctrl && ctrl.isScrubbing && ctrl.targetTime >= 0) {
track.trackTime = ctrl.targetTime;
}
else if (ctrl && !ctrl.isPlaying) {
if (skeleton.update) skeleton.update(0);
state.update(0);
}
else {
state.update(dt);
}
} else {
state.update(dt);
}
state.apply(skeleton);
// 物理更新逻辑
if(skeleton.update && (!ctrl || (ctrl.isPlaying && !ctrl.isScrubbing))) {
skeleton.update(dt);
}
if(spine.Physics) skeleton.updateWorldTransform(spine.Physics.update);
else skeleton.updateWorldTransform(1);
if(!Number.isFinite(cam.zoom)) cam.zoom=0.1;
const w = canvas.width, h = canvas.height;
Mat4.ortho(mvp, cam.x - w/cam.zoom/2, cam.x + w/cam.zoom/2, cam.y - h/cam.zoom/2, cam.y + h/cam.zoom/2, -2000, 2000);
shader.bind();
shader.setUniform4x4f(spine.Shader.MVP_MATRIX, mvp);
shader.setUniformi(spine.Shader.SAMPLER, 0);
// 从全局配置读取 PMA
const pma = window.viewerConfig ? window.viewerConfig.pmaEnabled : true;
gl.enable(gl.BLEND);
gl.blendFunc(pma ? gl.ONE : gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
batcher.begin(shader);
renderer.draw(batcher, skeleton);
batcher.end();
shader.unbind();
// === 绘制骨骼调试线 (导出时不绘制) ===
if(!isExporting) {
if(window.debugRenderer && window.viewerConfig.showBones) {
window.debugRenderer.drawBones(
skeleton,
cam.x,
cam.y,
cam.zoom,
h
);
} else if(window.debugRenderer) {
window.debugRenderer.drawBones(null, 0,0,1,0);
}
}
} catch(e) { }
}
};