| ---
|
| import MessageBox from "@/components/common/PioMessageBox.astro";
|
| import { spineModelConfig } from "@/config/pioConfig";
|
| import { url } from "@/utils/url-utils";
|
| ---
|
|
|
| {
|
| spineModelConfig.enable && (
|
| <div
|
| id="spine-model-container"
|
| style={`
|
| position: fixed;
|
| ${spineModelConfig.position.corner.includes("right") ? "right" : "left"}: ${spineModelConfig.position.offsetX}px;
|
| ${spineModelConfig.position.corner.includes("top") ? "top" : "bottom"}: ${spineModelConfig.position.offsetY}px;
|
| width: ${spineModelConfig.size.width}px;
|
| height: ${spineModelConfig.size.height}px;
|
| pointer-events: auto;
|
| z-index: 1000;
|
| `}
|
| >
|
| <div id="spine-player-container" style="width: 100%; height: 100%;" />
|
| <div id="spine-error" style="display: none;" />
|
| </div>
|
| )
|
| }
|
|
|
|
|
| <MessageBox />
|
|
|
| <script is:inline define:vars={{ spineModelConfig, modelPath: url(spineModelConfig.model.path), atlasPath: url(spineModelConfig.model.path.replace(".json", ".atlas")), cssPath: url("/pio/static/spine-player.min.css"), jsPath: url("/pio/static/spine-player.min.js") }}>
|
|
|
| function loadSpineCSS() {
|
| if (!spineModelConfig.enable) return;
|
|
|
|
|
| const existingLink = document.querySelector('link[href*="spine-player"]');
|
| if (existingLink) return;
|
|
|
|
|
| const cdnLink = document.createElement("link");
|
| cdnLink.rel = "stylesheet";
|
| cdnLink.href =
|
| "https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/spine-player.min.css";
|
|
|
|
|
| cdnLink.onerror = function () {
|
| console.warn("⚠️ Spine CSS CDN failed, trying local fallback...");
|
|
|
|
|
| if (cdnLink.parentNode) {
|
| cdnLink.parentNode.removeChild(cdnLink);
|
| }
|
|
|
|
|
| const localLink = document.createElement("link");
|
| localLink.rel = "stylesheet";
|
| localLink.href = cssPath;
|
| localLink.onerror = function () {
|
| console.error("❌ Failed to load Spine CSS");
|
| };
|
|
|
| document.head.appendChild(localLink);
|
| };
|
|
|
| document.head.appendChild(cdnLink);
|
| }
|
|
|
|
|
| let isClickProcessing = false;
|
| let lastClickTime = 0;
|
|
|
|
|
| window.spineModelInitialized = window.spineModelInitialized || false;
|
| window.spinePlayerInstance = window.spinePlayerInstance || null;
|
|
|
|
|
| function showMessage(message) {
|
|
|
| if (window.showModelMessage) {
|
| window.showModelMessage(message, {
|
| containerId: "spine-model-container",
|
| displayTime: spineModelConfig.interactive.messageDisplayTime || 3000
|
| });
|
| }
|
| }
|
|
|
|
|
| function updateResponsiveDisplay() {
|
| if (!spineModelConfig.enable) return;
|
|
|
| const container = document.getElementById("spine-model-container");
|
| if (!container) return;
|
|
|
|
|
| if (
|
| spineModelConfig.responsive.hideOnMobile &&
|
| window.innerWidth <= spineModelConfig.responsive.mobileBreakpoint
|
| ) {
|
| container.style.display = "none";
|
| } else {
|
| container.style.display = "block";
|
| }
|
| }
|
|
|
|
|
| function cleanupSpineModel() {
|
| console.log("🧹 Cleaning up existing Spine model...");
|
|
|
|
|
| if (window.clearModelMessage) {
|
| window.clearModelMessage();
|
| }
|
|
|
|
|
| if (window.spinePlayerInstance) {
|
| try {
|
| if (window.spinePlayerInstance.dispose) {
|
| window.spinePlayerInstance.dispose();
|
| }
|
| } catch (e) {
|
| console.warn("Error disposing spine player:", e);
|
| }
|
| window.spinePlayerInstance = null;
|
| }
|
|
|
|
|
| const playerContainer = document.getElementById("spine-player-container");
|
| if (playerContainer) {
|
| playerContainer.innerHTML = "";
|
| }
|
|
|
|
|
| window.spineModelInitialized = false;
|
| }
|
|
|
| async function initSpineModel() {
|
| if (!spineModelConfig.enable) return;
|
|
|
|
|
| if (
|
| spineModelConfig.responsive.hideOnMobile &&
|
| window.innerWidth <= spineModelConfig.responsive.mobileBreakpoint
|
| ) {
|
| console.log("📱 Mobile device detected, skipping Spine model initialization");
|
| const container = document.getElementById("spine-model-container");
|
| if (container) container.style.display = "none";
|
| return;
|
| }
|
|
|
|
|
| if (window.spineModelInitialized) {
|
| console.log("⏭️ Spine model already initialized, skipping...");
|
| return;
|
| }
|
|
|
| console.log("🎯 Initializing Spine Model...");
|
|
|
|
|
| cleanupSpineModel();
|
|
|
|
|
| loadSpineCSS();
|
|
|
|
|
| const loadSpineRuntime = () => {
|
| return new Promise((resolve, reject) => {
|
| if (typeof window.spine !== "undefined") {
|
| console.log("✅ Spine runtime already loaded");
|
| resolve();
|
| return;
|
| }
|
|
|
| console.log("📦 Loading Spine runtime...");
|
| const script = document.createElement("script");
|
| script.src =
|
| "https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/iife/spine-player.min.js";
|
| script.onload = () => {
|
| console.log("✅ Spine runtime loaded from CDN");
|
| resolve();
|
| };
|
| script.onerror = (_error) => {
|
| console.warn("⚠️ CDN failed, trying local fallback...");
|
|
|
|
|
| const fallbackScript = document.createElement("script");
|
| fallbackScript.src = jsPath;
|
| fallbackScript.onload = () => {
|
| console.log("✅ Spine runtime loaded from local fallback");
|
| resolve();
|
| };
|
| fallbackScript.onerror = () => {
|
| reject(new Error("Failed to load Spine runtime"));
|
| };
|
| document.head.appendChild(fallbackScript);
|
| };
|
| document.head.appendChild(script);
|
| });
|
| };
|
|
|
|
|
| const waitForSpine = () => {
|
| return new Promise((resolve, reject) => {
|
| let attempts = 0;
|
| const maxAttempts = 50;
|
|
|
| const check = () => {
|
| attempts++;
|
| if (typeof window.spine !== "undefined" && window.spine.SpinePlayer) {
|
| console.log("✅ Spine runtime loaded");
|
| resolve();
|
| } else if (attempts >= maxAttempts) {
|
| reject(new Error("Spine runtime loading timeout"));
|
| } else {
|
| setTimeout(check, 100);
|
| }
|
| };
|
| check();
|
| });
|
| };
|
|
|
| try {
|
|
|
| await loadSpineRuntime();
|
|
|
|
|
| await waitForSpine();
|
|
|
|
|
| window.spineModelInitialized = true;
|
|
|
|
|
| new window.spine.SpinePlayer("spine-player-container", {
|
| skeleton: modelPath,
|
| atlas: atlasPath,
|
| animation: "idle",
|
| backgroundColor: "#00000000",
|
| showControls: false,
|
| alpha: true,
|
| premultipliedAlpha: false,
|
| success: (player) => {
|
| console.log("🎉 Spine model loaded successfully!");
|
|
|
|
|
| window.spinePlayerInstance = player;
|
|
|
|
|
| setTimeout(() => {
|
| if (player.skeleton) {
|
| try {
|
| player.skeleton.updateWorldTransform();
|
| player.skeleton.setToSetupPose();
|
| } catch (e) {
|
| console.warn("Error positioning skeleton:", e);
|
| }
|
| }
|
| }, 500);
|
|
|
|
|
| if (spineModelConfig.interactive.enabled) {
|
| const canvas = document.querySelector(
|
| "#spine-player-container canvas"
|
| );
|
| if (canvas) {
|
| canvas.addEventListener("click", () => {
|
|
|
| const currentTime = Date.now();
|
| if (isClickProcessing || currentTime - lastClickTime < 500) {
|
| return;
|
| }
|
|
|
| isClickProcessing = true;
|
| lastClickTime = currentTime;
|
|
|
|
|
| const clickAnims =
|
| spineModelConfig.interactive.clickAnimations ||
|
| (spineModelConfig.interactive.clickAnimation
|
| ? [spineModelConfig.interactive.clickAnimation]
|
| : []);
|
|
|
| if (clickAnims.length > 0) {
|
| try {
|
| const randomClickAnim =
|
| clickAnims[Math.floor(Math.random() * clickAnims.length)];
|
| player.setAnimation(randomClickAnim, false);
|
|
|
|
|
| setTimeout(() => {
|
| const idleAnims =
|
| spineModelConfig.interactive.idleAnimations;
|
| const randomIdle =
|
| idleAnims[Math.floor(Math.random() * idleAnims.length)];
|
| player.setAnimation(randomIdle, true);
|
| }, 2000);
|
| } catch (e) {
|
| console.warn("Failed to play click animation:", e);
|
| }
|
| }
|
|
|
|
|
| const messages = spineModelConfig.interactive.clickMessages;
|
| if (messages && messages.length > 0) {
|
| const randomMessage =
|
| messages[Math.floor(Math.random() * messages.length)];
|
| showMessage(randomMessage);
|
| }
|
|
|
|
|
| setTimeout(() => {
|
| isClickProcessing = false;
|
| }, 500);
|
| });
|
|
|
|
|
| if (spineModelConfig.interactive.idleAnimations.length > 1) {
|
| setInterval(() => {
|
| try {
|
| const idleAnims =
|
| spineModelConfig.interactive.idleAnimations;
|
| const randomIdle =
|
| idleAnims[Math.floor(Math.random() * idleAnims.length)];
|
| player.setAnimation(randomIdle, true);
|
| } catch (e) {
|
| console.warn("Failed to play idle animation:", e);
|
| }
|
| }, spineModelConfig.interactive.idleInterval);
|
| }
|
| }
|
| }
|
|
|
| console.log("✅ Spine model setup complete!");
|
| },
|
| error: (_player, reason) => {
|
| console.error("❌ Spine model loading error:", reason);
|
|
|
| const errorDiv = document.getElementById("spine-error");
|
| if (errorDiv) {
|
| errorDiv.style.display = "block";
|
| errorDiv.innerHTML = `
|
| <div style="color: #ff4444; padding: 20px; text-align: center; font-size: 14px;">
|
| <div>⚠️ Spine 模型加载失败</div>
|
| <div style="font-size: 12px; margin-top: 8px; color: #888;">${reason}</div>
|
| </div>
|
| `;
|
| }
|
|
|
| const canvas = document.getElementById("spine-canvas");
|
| if (canvas) canvas.style.display = "none";
|
| },
|
| });
|
| } catch (error) {
|
| console.error("Spine model initialization error:", error);
|
|
|
|
|
| window.spineModelInitialized = false;
|
|
|
| const errorDiv = document.getElementById("spine-error");
|
| if (errorDiv) {
|
| errorDiv.style.display = "block";
|
| errorDiv.innerHTML = `
|
| <div style="color: #ff4444; padding: 20px; text-align: center; font-size: 14px;">
|
| <div>⚠️ Spine 运行时加载失败</div>
|
| <div style="font-size: 12px; margin-top: 8px; color: #888;">${error instanceof Error ? error.message : "未知错误"}</div>
|
| </div>
|
| `;
|
| }
|
| }
|
| }
|
|
|
|
|
| window.addEventListener("beforeunload", cleanupSpineModel);
|
|
|
|
|
| if (typeof window.swup !== "undefined" && window.swup.hooks) {
|
| window.swup.hooks.on("content:replace", () => {
|
|
|
| setTimeout(() => {
|
| updateResponsiveDisplay();
|
| }, 100);
|
| });
|
| }
|
|
|
|
|
| window.addEventListener("popstate", () => {
|
| setTimeout(() => {
|
| updateResponsiveDisplay();
|
| }, 100);
|
| });
|
|
|
|
|
| window.addEventListener("resize", updateResponsiveDisplay);
|
|
|
|
|
| if (document.readyState === "loading") {
|
| document.addEventListener("DOMContentLoaded", initSpineModel);
|
| } else {
|
| initSpineModel();
|
| }
|
| </script>
|
| |