@sanyueqi/web-components
Version:
Web components
478 lines (421 loc) • 18.3 kB
JavaScript
(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;
}
}));