UNPKG

@sanyueqi/web-components

Version:

Web components

478 lines (421 loc) 18.3 kB
(function (factory) { typeof define === 'function' && define.amd ? define(factory) : factory(); })((function () { 'use strict'; // 默认的Spine包地址 const DEFAULT_SPINE_URL = { cdn: 'https://unpkg.com/@esotericsoftware/spine-webcomponents@4.2.*/dist/iife/spine-webcomponents.min.js', local: '/public/lib/spine-webcomponents.min.js', }; /** * Spine动画管理器类 * 用于管理和渲染多个Spine动画 */ class SpineManager { animationGroupId; container; spinePackageUrl; animations = []; /** * 构造函数 * @param {string} animationGroupId - 动画组ID * @param {string|Object} container - 容器元素ID * @param {string|Object} spinePackageUrl - Spine包地址 */ constructor(container, animationGroupId = 'default', spinePackageUrl = DEFAULT_SPINE_URL) { if (animationGroupId) { this.setAnimationGroupId(animationGroupId); } if (container) { this.setContainer(container); } this.spinePackageUrl = spinePackageUrl; this.animations = []; this.initialized = false; this._loadCallbacks = { resolve: null, reject: null, }; } setAnimationGroupId(animationGroupId) { this.animationGroupId = animationGroupId; } setContainer(container) { this.container = typeof container === 'string' ? document.querySelector(container) : container; if (!this.container) { throw new Error(`找不到容器元素`) } } /** * 初始化Spine管理器 * @param {Array} animations - 动画配置数组 * @param {Boolean} noOverlay - 动画配置数组 * @returns {Promise<boolean>} 初始化是否成功 */ async init(animations = this.animations || [], noOverlay = false) { try { // 检查window.spine是否存在 if (!window.spine) { console.log('SpineManager: window.spine不存在,开始动态加载...'); await this.waitForSpine(); } this.animations = animations; this.renderAnimations(); if (!noOverlay) { this.addOverlay(); } this.initialized = true; console.log('SpineManager: 初始化成功'); return true } catch (error) { console.error('SpineManager: 初始化失败:', error); throw error } } /** * 等待Spine库加载 * @returns {Promise} */ waitForSpine() { return new Promise(async (resolve, reject) => { // 如果已经存在,直接返回 if (window.spine) { resolve(); return } // 存储回调以便脚本加载完成后调用 this._loadCallbacks.resolve = resolve; this._loadCallbacks.reject = reject; try { // 根据传入的包地址类型进行处理 if (typeof this.spinePackageUrl === 'string') { await this.loadScript(this.spinePackageUrl); } else if (this.spinePackageUrl && typeof this.spinePackageUrl === 'object') { // 优先尝试CDN try { await this.loadScript(this.spinePackageUrl.cdn); } catch (cdnError) { console.warn('SpineManager: CDN加载失败,尝试本地加载:', cdnError); await this.loadScript(this.spinePackageUrl.local); } } else { reject(new Error('无效的spine包地址配置')); return } } catch (error) { reject(error); } }) } /** * 动态加载脚本 * @param {string} url - 脚本URL * @returns {Promise} */ loadScript(url) { return new Promise((resolve, reject) => { // 检查是否已经加载过相同的脚本 const existingScript = document.querySelector(`script[src="${url}"]`); if (existingScript) { // 如果脚本已经在加载中或已加载,等待其完成 if (window.spine) { resolve(); } else { // 监听全局spine加载事件 const spineLoadedHandler = () => { document.removeEventListener('spine-loaded', spineLoadedHandler); resolve(); }; document.addEventListener('spine-loaded', spineLoadedHandler); // 设置超时 setTimeout(() => { document.removeEventListener('spine-loaded', spineLoadedHandler); reject(new Error('等待spine加载超时')); }, 10000); } return } const script = document.createElement('script'); script.src = url; script.onload = () => { // 等待spine库完全初始化 this.waitForSpineInitialization() .then(() => { // 触发spine加载完成事件 document.dispatchEvent(new CustomEvent('spine-loaded')); if (this._loadCallbacks.resolve) { this._loadCallbacks.resolve(); } }) .catch(error => { if (this._loadCallbacks.reject) { this._loadCallbacks.reject(error); } }); }; script.onerror = () => { console.error('SpineManager: Spine脚本加载失败:', url); if (this._loadCallbacks.reject) { this._loadCallbacks.reject(new Error(`无法加载Spine脚本: ${url}`)); } }; document.head.appendChild(script); }) } /** * 等待spine初始化完成 * @returns {Promise} */ waitForSpineInitialization() { return new Promise((resolve, reject) => { const checkSpine = () => { if (window.spine) { resolve(); } else { setTimeout(checkSpine, 100); } }; // 设置超时 const timeout = setTimeout(() => { reject(new Error('等待spine初始化超时')); }, 10000); checkSpine(); // 清除超时 window.addEventListener('spine-loaded', () => { clearTimeout(timeout); resolve(); }); }) } /** * 渲染所有动画 */ renderAnimations() { if (!this.container) { console.error('SpineManager: 容器不存在:', this.container); return } // 清空容器 this.container.innerHTML = ''; this.animations.forEach(anim => { const animContainer = document.createElement('div'); animContainer.id = anim.id; // 默认样式 const defaultStyles = { position: 'absolute', width: '100%', height: '100%', overflow: 'hidden' }; // 应用默认样式 Object.keys(defaultStyles).forEach(key => { animContainer.style[key] = defaultStyles[key]; }); // 应用自定义样式属性 const styleKeys = [ 'position', 'width', 'height', 'overflow', 'top', 'bottom', 'left', 'right', 'display', 'zIndex', 'margin', 'padding', 'background', 'opacity', 'transform' ]; styleKeys.forEach(key => { if (anim[key] !== undefined) { animContainer.style[key] = anim[key]; } }); // 创建spine-skeleton元素 const skeleton = document.createElement('spine-skeleton'); // 设置基本属性 skeleton.setAttribute('identifier', anim.id); skeleton.setAttribute('atlas', anim.atlas); skeleton.setAttribute('skeleton', anim.skeleton); skeleton.setAttribute('overlay-id', this.animationGroupId); // 设置可选属性 if (anim.animation) skeleton.setAttribute('animation', anim.animation); if (anim.scale) skeleton.setAttribute('scale', anim.scale); if (anim.fit) skeleton.setAttribute('fit', anim.fit); if (anim['offset-x']) skeleton.setAttribute('offset-x', anim['offset-x']); if (anim['offset-y']) skeleton.setAttribute('offset-y', anim['offset-y']); if (anim['x-axis']) skeleton.setAttribute('x-axis', anim['x-axis']); if (anim['y-axis']) skeleton.setAttribute('y-axis', anim['y-axis']); if (anim.drag) skeleton.setAttribute('drag', ''); if (anim.clip) skeleton.setAttribute('clip', ''); if (anim.skin) skeleton.setAttribute('skin', anim.skin); if (anim['raw-data']) skeleton.setAttribute('raw-data', anim['raw-data']); if (anim['json-skeleton-key']) skeleton.setAttribute('json-skeleton-key', anim['json-skeleton-key']); // 设置额外数据 if (anim.extraData) { Object.keys(anim.extraData).forEach(key => { skeleton.setAttribute(key, anim.extraData[key]); }); } animContainer.appendChild(skeleton); this.container.appendChild(animContainer); }); } /** * 添加overlay元素 */ addOverlay() { const overlay = document.createElement('spine-overlay'); overlay.setAttribute('overlay-id', this.animationGroupId); this.container.appendChild(overlay); } /** * 根据ID获取spine-skeleton元素 * @param {string} id - 动画ID * @returns {Element|null} spine-skeleton元素 */ getSkeleton(id) { if (!this.initialized) { console.warn('SpineManager: 尚未初始化'); return null } const skeleton = this.container.querySelector(`spine-skeleton[identifier="${id}"]`); return skeleton } /** * 添加动画 * @param {Object} animationConfig - 动画配置 */ addAnimation(animationConfig) { if (!animationConfig.id) { console.error('SpineManager: 动画配置必须包含id字段'); return } this.animations.push(animationConfig); if (this.initialized) { this.renderAnimations(); this.addOverlay(); } } /** * 移除动画 * @param {string} id - 要移除的动画ID */ removeAnimation(id) { this.animations = this.animations.filter(anim => anim.id !== id); if (this.initialized) { this.renderAnimations(); this.addOverlay(); } } /** * 获取所有动画配置 * @returns {Array} 动画配置数组 */ getAnimations() { return [...this.animations] } /** * 检查是否已初始化 * @returns {boolean} 是否已初始化 */ isInitialized() { return this.initialized } /** * 销毁Spine管理器 * 清理所有动画和事件监听器 */ async destroy() { await this.disposeAllSpineResources(); if (this.container) { this.container.innerHTML = ''; } this.initialized = false; this._loadCallbacks.resolve = null; this._loadCallbacks.reject = null; } /** * 释放指定容器内的所有Spine资源 * @param {string|Object} _container - 外层容器ID */ async disposeAllSpineResources(_container = this.container) { const container = typeof _container === 'string' ? document.querySelector(_container) : _container; if (!container) { console.warn(`容器未找到`); return; } try { // 1. 释放所有spine-skeleton组件 const skeletons = container.querySelectorAll('spine-skeleton'); const disposePromises = []; for (const skeleton of skeletons) { try { const identifier = skeleton.getAttribute('identifier'); if (identifier && window.spine && window.spine.getSkeleton) { // 通过spine API获取实例并释放 const spineInstance = window.spine.getSkeleton(identifier); if (spineInstance && spineInstance.whenReady) { const disposePromise = spineInstance.whenReady.then(instance => { if (instance && typeof instance.dispose === 'function') { instance.dispose(); } skeleton.remove(); // 从DOM移除 }).catch(error => { console.warn(`释放骨架 ${identifier} 时出错:`, error); skeleton.remove(); // 即使出错也移除元素 }); disposePromises.push(disposePromise); } else { // 如果没有whenReady,直接移除 skeleton.remove(); } } else { // 如果没有identifier或spine API,尝试直接调用dispose方法 if (skeleton.dispose && typeof skeleton.dispose === 'function') { skeleton.dispose(); } skeleton.remove(); } } catch (error) { console.error(`处理骨架时出错:`, error); skeleton.remove(); // 确保元素被移除 } } // 等待所有骨架释放完成 await Promise.allSettled(disposePromises); // 2. 释放spine-overlay const overlay = container.querySelector('spine-overlay'); if (overlay) { try { if (overlay.dispose && typeof overlay.dispose === 'function') { overlay.dispose(); } overlay.remove(); } catch (error) { console.error('释放overlay时出错:', error); overlay.remove(); // 确保元素被移除 } } } catch (error) { console.error('释放Spine资源过程中发生错误:', error); } } /** * 带重试机制的Spine资源释放 */ async disposeAllSpineResourcesWithRetry(container, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { await this.disposeAllSpineResources(container); break; // 成功则退出循环 } catch (error) { if (attempt === maxRetries) { throw error; } // 等待一段时间后重试 await new Promise(resolve => setTimeout(resolve, 100 * attempt)); } } } } // 为了兼容性,同时支持CommonJS和浏览器全局变量 if (typeof module !== 'undefined' && module.exports) { module.exports = SpineManager; } if (typeof window !== 'undefined') { window.SpineManager = SpineManager; } }));