Files
tools/Spine插件/Spine_动画播放器_2.0/viewer_4.1.html
T

303 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Spine 4.1 Core</title>
<script src="spine-webgl.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; right: 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:#00e5ff; margin-bottom:10px;">SPINE 4.1 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="-500" 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, renderer, assetManager, skeleton, animationState;
var lastTime = Date.now();
window.onload = function() {
try {
if (typeof spine === 'undefined') {
throw new Error("spine-webgl.js (4.1) 未加载!\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 });
renderer = new spine.SceneRenderer(canvas, gl);
assetManager = new spine.AssetManager(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.GLTexture(gl, img);
const spineAtlas = new spine.TextureAtlas(atlasText, (path) => texture);
for (let i = 0; i < spineAtlas.pages.length; i++) {
spineAtlas.pages[i].texture = texture;
spineAtlas.pages[i].width = img.width;
spineAtlas.pages[i].height = img.height;
}
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;
const jsonObj = JSON.parse(text);
if(jsonObj.skeleton) jsonObj.skeleton.images = "";
skeletonData = skeletonJson.readSkeletonData(jsonObj);
}
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 = -500;
renderer.camera.position.x = 0;
renderer.camera.position.y = 0;
renderer.camera.zoom = 1.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 = -500;
renderer.camera.position.x = 0; renderer.camera.position.y = 0; renderer.camera.zoom = 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) {
renderer.camera.position.x -= (e.clientX - startX) * renderer.camera.zoom;
renderer.camera.position.y += (e.clientY - startY) * renderer.camera.zoom;
startX = e.clientX; startY = e.clientY;
} else if(isRightDrag) {
let dy = e.clientY - startY;
let newZoom = renderer.camera.zoom * (1 + dy * 0.01);
if(newZoom > 0.1 && newZoom < 10) renderer.camera.zoom = newZoom;
startY = e.clientY;
}
};
canvas.onwheel = e => {
let newZoom = renderer.camera.zoom * (e.deltaY > 0 ? 1.1 : 0.9);
if(newZoom > 0.1 && newZoom < 10) renderer.camera.zoom = 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);
if(renderer && renderer.camera) {
if(renderer.camera.viewportWidth !== w || renderer.camera.viewportHeight !== h) {
renderer.camera.viewportWidth = w; renderer.camera.viewportHeight = h; renderer.camera.update();
}
}
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();
renderer.begin();
renderer.drawSkeleton(skeleton, true);
renderer.end();
}
requestAnimationFrame(render);
}
</script>
</body>
</html>