303 lines
11 KiB
HTML
303 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Spine 3.8 Core</title>
|
|
<script src="spine-webgl-3.8.js"></script>
|
|
<style>
|
|
body, html { margin: 0; padding: 0; width: 100%; height: 100%; background: transparent; overflow: hidden; color:#eee; font-family: sans-serif;}
|
|
canvas { display: block; width: 100%; height: 100%; }
|
|
|
|
#ui { position: absolute; top: 20px; left: 20px; background: rgba(0,0,0,0.7); padding: 20px; border-radius: 8px; display: none; min-width: 200px; z-index: 10;}
|
|
#controls { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); padding: 15px; border-radius: 10px; color: white; display: none; gap: 30px; z-index: 10; align-items: center;}
|
|
.control-group { display: flex; flex-direction: column; align-items: center; gap: 5px; }
|
|
input[type=range] { width: 200px; cursor: pointer; }
|
|
select { background: #444; color: white; border: 1px solid #666; padding: 5px; border-radius: 4px; width: 100%; margin-top: 5px;}
|
|
button { cursor: pointer; border:none; border-radius:4px; padding:5px 10px;}
|
|
|
|
/* 加载提示 */
|
|
#loading { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; color: #00e5ff; background: rgba(0,0,0,0.8); padding: 20px 40px; border-radius: 10px; z-index: 100; pointer-events: none; text-align: center; }
|
|
#error-box { position: fixed; top: 10%; left: 10%; right: 10%; bottom: 10%; background: rgba(50,0,0,0.95); color: #ff5555; padding: 20px; border: 2px solid red; overflow: auto; display: none; z-index: 200; font-family: monospace; white-space: pre-wrap; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="loading">⏳ 初始化内核...</div>
|
|
<div id="error-box"></div>
|
|
|
|
<div id="ui">
|
|
<div style="font-weight:bold; color:#ff0055; margin-bottom:10px;">SPINE 3.8 DETECTED</div>
|
|
<div style="margin-bottom:10px;">
|
|
<label>动画:</label> <select id="anim-select"></select>
|
|
</div>
|
|
<div style="margin-bottom:10px;">
|
|
<label>皮肤:</label> <select id="skin-select"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="controls">
|
|
<div class="control-group">
|
|
<label>X</label> <input type="range" id="posX" min="-2000" max="2000" value="0" step="10">
|
|
</div>
|
|
<div class="control-group">
|
|
<label>Y</label> <input type="range" id="posY" min="-2000" max="2000" value="0" step="10">
|
|
</div>
|
|
<div class="control-group">
|
|
<button onclick="resetView()" style="background:#555; color:white;">重置</button>
|
|
</div>
|
|
</div>
|
|
|
|
<canvas id="canvas"></canvas>
|
|
|
|
<script>
|
|
// 全局错误捕捉
|
|
window.onerror = function(msg, url, line, col, error) {
|
|
showError(`运行错误:\n${msg}\nLine: ${line}`);
|
|
return false;
|
|
};
|
|
|
|
function showStatus(msg) {
|
|
document.getElementById('loading').innerText = msg;
|
|
document.getElementById('loading').style.display = 'block';
|
|
}
|
|
|
|
function hideStatus() {
|
|
document.getElementById('loading').style.display = 'none';
|
|
}
|
|
|
|
function showError(msg) {
|
|
const box = document.getElementById('error-box');
|
|
box.style.display = 'block';
|
|
box.innerText += msg + "\n----------------\n";
|
|
hideStatus();
|
|
}
|
|
|
|
var canvas, gl, shader, batcher, mvp, skeletonRenderer;
|
|
var skeleton, animationState;
|
|
var lastTime = Date.now();
|
|
var camX=0, camY=0, camZoom=1.0;
|
|
|
|
window.onload = function() {
|
|
try {
|
|
if (typeof spine === 'undefined') {
|
|
throw new Error("spine-webgl-3.8.js 未加载!\n请检查 Spine_Universal 文件夹下是否存在该文件。");
|
|
}
|
|
|
|
init();
|
|
showStatus("正在连接主页面...");
|
|
|
|
if (window.parent && window.parent.spineSharedData) {
|
|
loadFromSharedData(window.parent.spineSharedData);
|
|
} else {
|
|
// 稍微延迟一下再次尝试,防止加载时序问题
|
|
setTimeout(() => {
|
|
if (window.parent && window.parent.spineSharedData) {
|
|
loadFromSharedData(window.parent.spineSharedData);
|
|
} else {
|
|
showError("无法获取文件数据!\n原因可能是浏览器限制了本地 iframe 访问。\n请尝试使用 Chrome 并在启动参数添加 --allow-file-access-from-files (不推荐),或者这是第一次加载太慢。");
|
|
}
|
|
}, 500);
|
|
}
|
|
} catch(e) {
|
|
showError(e.toString());
|
|
}
|
|
};
|
|
|
|
async function init() {
|
|
canvas = document.getElementById("canvas");
|
|
gl = canvas.getContext("webgl", { alpha: false });
|
|
|
|
shader = spine.webgl.Shader.newTwoColoredTextured(gl);
|
|
batcher = new spine.webgl.PolygonBatcher(gl);
|
|
mvp = new spine.webgl.Matrix4();
|
|
skeletonRenderer = new spine.webgl.SkeletonRenderer(gl);
|
|
|
|
requestAnimationFrame(render);
|
|
}
|
|
|
|
// 辅助函数
|
|
function readFileAsText(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const fr = new FileReader();
|
|
fr.onload = () => resolve(fr.result);
|
|
fr.onerror = () => reject("读取文本失败");
|
|
fr.readAsText(file);
|
|
});
|
|
}
|
|
function readFileAsArrayBuffer(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const fr = new FileReader();
|
|
fr.onload = () => resolve(fr.result);
|
|
fr.onerror = () => reject("读取二进制失败");
|
|
fr.readAsArrayBuffer(file);
|
|
});
|
|
}
|
|
function readFileAsDataURL(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const fr = new FileReader();
|
|
fr.onload = () => resolve(fr.result);
|
|
fr.onerror = () => reject("读取图片失败");
|
|
fr.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
async function loadFromSharedData(data) {
|
|
try {
|
|
const { atlas, png, main, type } = data;
|
|
|
|
showStatus("正在读取 Atlas...");
|
|
const atlasText = await readFileAsText(atlas);
|
|
|
|
showStatus("正在读取图片...");
|
|
const imgUrl = await readFileAsDataURL(png);
|
|
|
|
const img = new Image();
|
|
img.onload = async () => {
|
|
try {
|
|
showStatus("构建纹理...");
|
|
const texture = new spine.webgl.GLTexture(gl, img);
|
|
const spineAtlas = new spine.TextureAtlas(atlasText, (path) => texture);
|
|
const atlasLoader = new spine.AtlasAttachmentLoader(spineAtlas);
|
|
|
|
let skeletonData;
|
|
|
|
if(type === 'binary') {
|
|
showStatus("解析二进制骨架...");
|
|
const buffer = await readFileAsArrayBuffer(main);
|
|
const skeletonBinary = new spine.SkeletonBinary(atlasLoader);
|
|
skeletonBinary.scale = 1.0;
|
|
skeletonData = skeletonBinary.readSkeletonData(new Uint8Array(buffer));
|
|
} else {
|
|
showStatus("解析 JSON 骨架...");
|
|
const text = await readFileAsText(main);
|
|
const skeletonJson = new spine.SkeletonJson(atlasLoader);
|
|
skeletonJson.scale = 1.0;
|
|
skeletonData = skeletonJson.readSkeletonData(JSON.parse(text));
|
|
}
|
|
|
|
showStatus("初始化渲染器...");
|
|
setupSkeleton(skeletonData);
|
|
hideStatus();
|
|
|
|
document.getElementById('ui').style.display = 'block';
|
|
document.getElementById('controls').style.display = 'flex';
|
|
} catch(err) {
|
|
showError("解析过程错误:\n" + err);
|
|
}
|
|
};
|
|
img.onerror = () => showError("图片加载失败!");
|
|
img.src = imgUrl;
|
|
|
|
} catch(e) {
|
|
showError("数据加载错误:\n" + e);
|
|
}
|
|
}
|
|
|
|
function setupSkeleton(skeletonData) {
|
|
skeleton = new spine.Skeleton(skeletonData);
|
|
skeleton.setToSetupPose();
|
|
skeleton.updateWorldTransform();
|
|
skeleton.x = 0; skeleton.y = 0;
|
|
|
|
var stateData = new spine.AnimationStateData(skeleton.data);
|
|
animationState = new spine.AnimationState(stateData);
|
|
|
|
const animSelect = document.getElementById('anim-select');
|
|
animSelect.innerHTML = "";
|
|
skeleton.data.animations.forEach(a => {
|
|
const opt = document.createElement('option');
|
|
opt.value = a.name; opt.text = a.name; animSelect.appendChild(opt);
|
|
});
|
|
if(skeleton.data.animations.length > 0) animationState.setAnimation(0, skeleton.data.animations[0].name, true);
|
|
|
|
const skinSelect = document.getElementById('skin-select');
|
|
skinSelect.innerHTML = "";
|
|
skeleton.data.skins.forEach(s => {
|
|
const opt = document.createElement('option');
|
|
opt.value = s.name; opt.text = s.name; skinSelect.appendChild(opt);
|
|
});
|
|
if(skeleton.data.defaultSkin) skinSelect.value = skeleton.data.defaultSkin.name;
|
|
|
|
animSelect.onchange = () => animationState.setAnimation(0, animSelect.value, true);
|
|
skinSelect.onchange = () => { skeleton.setSkinByName(skinSelect.value); skeleton.setSlotsToSetupPose(); };
|
|
|
|
updateControls();
|
|
bindControls();
|
|
}
|
|
|
|
function updateControls() {
|
|
if(!skeleton) return;
|
|
document.getElementById('posX').value = skeleton.x;
|
|
document.getElementById('posY').value = skeleton.y;
|
|
}
|
|
|
|
function resetView() {
|
|
if(!skeleton) return;
|
|
skeleton.x = 0; skeleton.y = 0;
|
|
camX = 0; camY = 0; camZoom = 1.0;
|
|
updateControls();
|
|
}
|
|
|
|
function bindControls() {
|
|
const inputX = document.getElementById('posX');
|
|
const inputY = document.getElementById('posY');
|
|
inputX.oninput = () => { if(skeleton) skeleton.x = parseFloat(inputX.value); };
|
|
inputY.oninput = () => { if(skeleton) skeleton.y = parseFloat(inputY.value); };
|
|
|
|
let isDragging=false, startX, startY, isRightDrag=false;
|
|
canvas.oncontextmenu = e => e.preventDefault();
|
|
canvas.onmousedown = e => {
|
|
if(e.button === 2) isRightDrag = true; else isDragging = true;
|
|
startX = e.clientX; startY = e.clientY;
|
|
};
|
|
window.onmouseup = () => { isDragging=false; isRightDrag=false; };
|
|
window.onmousemove = e => {
|
|
if(isDragging) {
|
|
camX -= (e.clientX - startX) / camZoom;
|
|
camY += (e.clientY - startY) / camZoom;
|
|
startX = e.clientX; startY = e.clientY;
|
|
} else if(isRightDrag) {
|
|
let dy = e.clientY - startY;
|
|
let newZoom = camZoom * (1 + dy * 0.01);
|
|
if(newZoom > 0.1 && newZoom < 10) camZoom = newZoom;
|
|
startY = e.clientY;
|
|
}
|
|
};
|
|
canvas.onwheel = e => {
|
|
let newZoom = camZoom * (e.deltaY > 0 ? 0.9 : 1.1);
|
|
if(newZoom > 0.1 && newZoom < 10) camZoom = newZoom;
|
|
};
|
|
}
|
|
|
|
function render() {
|
|
let now = Date.now();
|
|
let delta = (now - lastTime) / 1000;
|
|
lastTime = now;
|
|
let w = window.innerWidth, h = window.innerHeight;
|
|
if (canvas.width != w || canvas.height != h) { canvas.width = w; canvas.height = h; }
|
|
gl.viewport(0, 0, w, h);
|
|
gl.clearColor(0.2, 0.2, 0.2, 1);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
|
|
if (skeleton && animationState) {
|
|
animationState.update(delta);
|
|
animationState.apply(skeleton);
|
|
skeleton.updateWorldTransform();
|
|
|
|
shader.bind();
|
|
mvp.ortho2d(camX - w/2/camZoom, camY - h/2/camZoom, w/camZoom, h/camZoom);
|
|
shader.setUniform4x4f(spine.webgl.Shader.MVP_MATRIX, mvp.values);
|
|
shader.setUniformi(spine.webgl.Shader.SAMPLER, 0);
|
|
gl.enable(gl.BLEND);
|
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
batcher.begin(shader);
|
|
skeletonRenderer.draw(batcher, skeleton);
|
|
batcher.end();
|
|
shader.unbind();
|
|
}
|
|
requestAnimationFrame(render);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|