@yqg/aminofx-css-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,276 @@
1
+ import type { IPlayerOptions } from './type';
2
+
3
+ // 扩展全局 Window 接口
4
+ declare global {
5
+ interface Window {
6
+ createCssAnimationPlayer: typeof createCssAnimationPlayer;
7
+ }
8
+ }
9
+
10
+ // 资源加载器
11
+ class ResourceLoader {
12
+ private static imageCache = new Map<string, boolean>();
13
+ private static loadingQueue = new Map<string, Promise<void>>();
14
+
15
+ static async loadImages(urls: string[]): Promise<void> {
16
+ if (!Array.isArray(urls) || urls.length === 0) {
17
+ return Promise.resolve();
18
+ }
19
+
20
+ const loadPromises = urls.map((url, index) => {
21
+ if (!url.startsWith('http')) {
22
+ return Promise.resolve();
23
+ }
24
+
25
+ if (this.imageCache.has(url)) {
26
+ console.log(`aminofx: ✅ 图片 ${index + 1} 已缓存: ${url}`);
27
+ return Promise.resolve();
28
+ }
29
+
30
+ if (this.loadingQueue.has(url)) {
31
+ return this.loadingQueue.get(url)!;
32
+ }
33
+
34
+ const loadPromise = new Promise<void>((resolve) => {
35
+ const img = new Image();
36
+
37
+ img.onload = () => {
38
+ this.imageCache.set(url, true);
39
+ this.loadingQueue.delete(url);
40
+ console.log(`aminofx: ✅ 图片 ${index + 1} 加载成功: ${url}`);
41
+
42
+ // 确保图片资源在加载后能够被浏览器解码(decode),以提升渲染性能和避免闪烁。
43
+ // 如果浏览器支持 Image.decode 方法,则异步等待 decode 完成后再进行后续处理;
44
+ // decode 失败时也会继续后续流程,并做降级提示。
45
+ // 对于不支持 decode 的环境,则直接进入兼容处理流程,保证流程不中断。
46
+ if (typeof img.decode === 'function') {
47
+ img
48
+ .decode()
49
+ .then(() => {
50
+ console.log(`aminofx: 🎨 图片 ${index + 1} 解码完成: ${url}`);
51
+ resolve();
52
+ })
53
+ .catch(() => {
54
+ console.log(
55
+ `aminofx: ⚠️ 图片 ${index + 1} decode失败,使用fallback: ${url}`,
56
+ );
57
+ resolve();
58
+ });
59
+ } else {
60
+ console.log(`aminofx: 📱 图片 ${index + 1} 使用兼容模式: ${url}`);
61
+ resolve();
62
+ }
63
+ };
64
+
65
+ img.src = url;
66
+
67
+ img.onerror = () => {
68
+ this.loadingQueue.delete(url);
69
+ console.error(`aminofx: ❌ 图片 ${index + 1} 加载失败: ${url}`);
70
+ // 失败时也要继续后续流程,保证流程不中断。后续图片会再加载一次
71
+ resolve();
72
+ };
73
+ });
74
+
75
+ this.loadingQueue.set(url, loadPromise);
76
+ return loadPromise;
77
+ });
78
+
79
+ try {
80
+ await Promise.all(loadPromises);
81
+ console.log('aminofx: 🎉 所有图片资源加载完成');
82
+ } catch (error) {
83
+ console.error('aminofx: ❌ 图片加载过程中出现错误:', error);
84
+ }
85
+ }
86
+ }
87
+
88
+ class AnimationPlayer {
89
+ private container: HTMLElement;
90
+ private options: IPlayerOptions;
91
+ private isReady = false;
92
+ private readyPromise: Promise<void> | null = null;
93
+ private loopTimerId: number | null = null; // 保存定时器ID,用于清理
94
+
95
+ constructor(container: HTMLElement, options: IPlayerOptions) {
96
+ this.container = container;
97
+ this.options = options;
98
+ this.init();
99
+ }
100
+
101
+ private async init(): Promise<void> {
102
+ if (this.options.jsonSchema) {
103
+ this.container.innerHTML = this.replacePlaceholders(
104
+ this.options.jsonSchema.content,
105
+ );
106
+ } else if (this.options?.jsonUrl) {
107
+ // 通过url拉取资源
108
+ const response = await fetch(this.options?.jsonUrl);
109
+ const data = await response.json();
110
+ this.options.jsonSchema = data;
111
+ this.container.innerHTML = this.replacePlaceholders(
112
+ this.options.jsonSchema?.content || '',
113
+ );
114
+ } else {
115
+ throw new Error('没有提供jsonSchema或jsonUrl');
116
+ }
117
+ await this.ready();
118
+ }
119
+
120
+ /**
121
+ * 高性能占位符替换方法
122
+ * 支持格式:{{key}} 或 ${key}
123
+ * @param content - 需要替换的内容
124
+ * @returns 替换后的内容
125
+ */
126
+ private replacePlaceholders(content: string): string {
127
+ if (!this.options.dynamicData || !content) {
128
+ return content;
129
+ }
130
+
131
+ // 使用正则表达式一次性匹配所有占位符 {{key}} 或 ${key}
132
+ // 采用惰性匹配,避免贪婪匹配导致的性能问题
133
+ return content.replace(
134
+ /\{\{(\w+)\}\}|\$\{(\w+)\}/g,
135
+ (match, key1, key2) => {
136
+ const key = key1 || key2;
137
+ return this.options.dynamicData?.[key] !== undefined
138
+ ? String(this.options.dynamicData[key])
139
+ : match;
140
+ },
141
+ );
142
+ }
143
+
144
+ private handleLoop() {
145
+ if (!this.options.autoLoop || !this.options.jsonSchema?.duration) {
146
+ return;
147
+ }
148
+
149
+ // 清理旧的定时器,防止累积
150
+ if (this.loopTimerId !== null) {
151
+ clearTimeout(this.loopTimerId);
152
+ this.loopTimerId = null;
153
+ console.log('aminofx: 🧹 清理旧的循环定时器');
154
+ }
155
+
156
+ // 后续根据使用情况看是否调整为监听css动画结束事件
157
+ this.loopTimerId = window.setTimeout(() => {
158
+ this.container.style.display = 'none';
159
+ window.setTimeout(() => {
160
+ this.loopTimerId = null;
161
+ this.play();
162
+ }, 100);
163
+ }, this.options.jsonSchema?.duration);
164
+
165
+ console.log(
166
+ `aminofx: 🔄 启动循环定时器,时长: ${this.options.jsonSchema?.duration}ms`,
167
+ );
168
+ }
169
+
170
+ async ready(): Promise<void> {
171
+ if (this.isReady) {
172
+ console.log('aminofx: ✅ 动画已准备就绪,直接返回');
173
+ return Promise.resolve();
174
+ }
175
+
176
+ if (this.readyPromise) {
177
+ console.log('aminofx: ⏳ 正在准备中,等待现有Promise...');
178
+ return this.readyPromise;
179
+ }
180
+
181
+ this.readyPromise = new Promise<void>(async (resolve) => {
182
+ try {
183
+ await ResourceLoader.loadImages(this.options.preloadImgList || []);
184
+ this.isReady = true; // 设置准备完成状态
185
+ console.log('aminofx: 🎉 动画准备完成,可以开始播放');
186
+ } catch (error) {
187
+ console.error('aminofx: ❌ 动画准备失败:', error);
188
+ }
189
+ resolve();
190
+ });
191
+
192
+ return this.readyPromise;
193
+ }
194
+
195
+ async play(): Promise<void> {
196
+ if (!this.isReady) {
197
+ console.warn('aminofx: ⚠️ 动画尚未准备就绪,自动调用ready()...');
198
+ await this.ready();
199
+ }
200
+
201
+ this.container.style.display = 'flex';
202
+
203
+ // 只在循环未启动时才启动循环,防止重复调用
204
+ if (this.options.autoLoop && this.loopTimerId === null) {
205
+ this.handleLoop();
206
+ }
207
+ }
208
+
209
+ destroy(): void {
210
+ // 清理循环定时器
211
+ if (this.loopTimerId !== null) {
212
+ clearTimeout(this.loopTimerId);
213
+ this.loopTimerId = null;
214
+ console.log('aminofx: 🧹 销毁时清理循环定时器');
215
+ }
216
+
217
+ // 重置状态
218
+ this.container.innerHTML = '';
219
+ this.isReady = false;
220
+ this.readyPromise = null;
221
+
222
+ console.log('aminofx: 🗑️ AnimationPlayer 已销毁');
223
+ }
224
+
225
+ updateSchema(options: IPlayerOptions): void {
226
+ // 清理旧的循环定时器
227
+ if (this.loopTimerId !== null) {
228
+ clearTimeout(this.loopTimerId);
229
+ this.loopTimerId = null;
230
+ console.log('aminofx: 🧹 更新Schema时清理循环定时器');
231
+ }
232
+
233
+ // 重置状态
234
+ this.isReady = false;
235
+ this.readyPromise = null;
236
+
237
+ // 更新配置并重新初始化
238
+ this.options = options;
239
+ this.init();
240
+ }
241
+ }
242
+
243
+ function createCssAnimationPlayer(
244
+ containerId: string,
245
+ options: IPlayerOptions,
246
+ ): AnimationPlayer {
247
+ const container = document.getElementById(containerId);
248
+ if (!container) {
249
+ throw new Error(`Container with id "${containerId}" not found`);
250
+ }
251
+
252
+ // 固定比例画布容器,通过设置container适配不同比例机型
253
+ const canvasContainer = document.createElement('div');
254
+
255
+ Object.assign(canvasContainer.style, {
256
+ display: 'none',
257
+ width: '100vw',
258
+ height: '100vh',
259
+ overflow: 'hidden',
260
+ alignItems: options.alignItems || 'center',
261
+ justifyContent: 'center',
262
+ backgroundColor: 'rgba(0, 0, 0, 0.85)',
263
+ backdropFilter: 'blur(10px)',
264
+ });
265
+
266
+ const instance = new AnimationPlayer(canvasContainer, options);
267
+
268
+ container.appendChild(canvasContainer);
269
+
270
+ return instance;
271
+ }
272
+
273
+ export default createCssAnimationPlayer;
274
+ window.createCssAnimationPlayer = createCssAnimationPlayer;
275
+ export { AnimationPlayer };
276
+ export * from './type';
package/src/type.ts ADDED
@@ -0,0 +1,22 @@
1
+ // JSON 类型定义
2
+ type IJsonSchema = {
3
+ duration: number;
4
+ content: string;
5
+ };
6
+
7
+ interface IPlayerOptions {
8
+ // 弹窗垂直对齐方式
9
+ alignItems?: 'center' | 'flex-start' | 'flex-end';
10
+ // 传入 JSON 数据
11
+ jsonSchema?: IJsonSchema;
12
+ // 传入 JSON 文件 URL,同时传入 jsonSchema 时,jsonSchema 优先级更高
13
+ jsonUrl?: string;
14
+ // 预加载图片列表
15
+ preloadImgList?: string[];
16
+ // 是否自动循环播放
17
+ autoLoop?: boolean;
18
+ // 动态变量
19
+ dynamicData?: Record<string, string | number | boolean>;
20
+ }
21
+
22
+ export type { IPlayerOptions, IJsonSchema };