canvasframework 0.4.11 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.4.11",
3
+ "version": "0.5.0",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -10,6 +10,249 @@ class AnimationEngine {
10
10
  this.animations = new Map();
11
11
  this.isRunning = false;
12
12
  this.animationFrameId = null;
13
+
14
+ // Batching
15
+ this.batchQueue = [];
16
+ this.batchScheduled = false;
17
+
18
+ // Worker pour les calculs d'animation
19
+ this.worker = null;
20
+ this.workerEnabled = true;
21
+ this.initWorker();
22
+
23
+ // OffscreenCanvas cache
24
+ this.offscreenCache = new Map();
25
+ }
26
+
27
+ /**
28
+ * Initialise le Web Worker pour les animations
29
+ * @private
30
+ */
31
+ initWorker() {
32
+ if (!window.Worker) {
33
+ this.workerEnabled = false;
34
+ console.warn('Web Workers non supportés, fallback sur le thread principal');
35
+ return;
36
+ }
37
+
38
+ // Créer le worker inline
39
+ const workerCode = `
40
+ // Fonctions d'easing dans le Worker
41
+ const easings = {
42
+ linear: t => t,
43
+ easeInQuad: t => t * t,
44
+ easeOutQuad: t => t * (2 - t),
45
+ easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
46
+ easeInCubic: t => t * t * t,
47
+ easeOutCubic: t => (--t) * t * t + 1,
48
+ easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
49
+ easeInQuart: t => t * t * t * t,
50
+ easeOutQuart: t => 1 - (--t) * t * t * t,
51
+ easeInOutQuart: t => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
52
+ easeInElastic: t => {
53
+ const c4 = (2 * Math.PI) / 3;
54
+ return t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
55
+ },
56
+ easeOutElastic: t => {
57
+ const c4 = (2 * Math.PI) / 3;
58
+ return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
59
+ },
60
+ easeOutBounce: t => {
61
+ const n1 = 7.5625;
62
+ const d1 = 2.75;
63
+ if (t < 1 / d1) return n1 * t * t;
64
+ else if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
65
+ else if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
66
+ else return n1 * (t -= 2.625 / d1) * t + 0.984375;
67
+ }
68
+ };
69
+
70
+ // Calculer les valeurs d'animation
71
+ self.onmessage = function(e) {
72
+ const { type, data } = e.data;
73
+
74
+ if (type === 'CALCULATE_BATCH') {
75
+ const { animations, currentTime } = data;
76
+ const results = [];
77
+
78
+ for (let anim of animations) {
79
+ // Gérer le délai
80
+ if (anim.isDelaying) {
81
+ if (!anim.delayStartTime) {
82
+ anim.delayStartTime = currentTime;
83
+ }
84
+
85
+ if (currentTime - anim.delayStartTime >= anim.delay) {
86
+ anim.isDelaying = false;
87
+ anim.startTime = currentTime;
88
+ } else {
89
+ results.push({ id: anim.id, status: 'delaying', animation: anim });
90
+ continue;
91
+ }
92
+ }
93
+
94
+ // Initialiser le temps de départ
95
+ if (!anim.startTime) {
96
+ anim.startTime = currentTime;
97
+ }
98
+
99
+ // Calculer la progression
100
+ const elapsed = currentTime - anim.startTime;
101
+ let progress = Math.min(elapsed / anim.duration, 1);
102
+
103
+ // Appliquer l'easing
104
+ const easingFunc = easings[anim.easing] || easings.linear;
105
+ const easedProgress = easingFunc(progress);
106
+
107
+ // Inverser si yoyo
108
+ const actualProgress = anim.isReversed ? 1 - easedProgress : easedProgress;
109
+
110
+ // Calculer les nouvelles valeurs
111
+ const values = {};
112
+ for (let prop in anim.to) {
113
+ const from = anim.from[prop];
114
+ const to = anim.to[prop];
115
+ values[prop] = from + (to - from) * actualProgress;
116
+ }
117
+
118
+ // Déterminer le statut
119
+ let status = 'running';
120
+ let newState = { ...anim };
121
+
122
+ if (progress >= 1) {
123
+ if (anim.yoyo && !anim.isReversed) {
124
+ status = 'yoyo';
125
+ newState.isReversed = true;
126
+ newState.startTime = currentTime;
127
+ } else if (anim.loop) {
128
+ status = 'loop';
129
+ newState.startTime = currentTime;
130
+ newState.isReversed = false;
131
+ } else {
132
+ status = 'complete';
133
+ }
134
+ }
135
+
136
+ results.push({
137
+ id: anim.id,
138
+ status,
139
+ values,
140
+ actualProgress,
141
+ animation: newState
142
+ });
143
+ }
144
+
145
+ self.postMessage({ type: 'BATCH_RESULT', results });
146
+ }
147
+ };
148
+ `;
149
+
150
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
151
+ const workerUrl = URL.createObjectURL(blob);
152
+
153
+ try {
154
+ this.worker = new Worker(workerUrl);
155
+
156
+ this.worker.onmessage = (e) => {
157
+ if (e.data.type === 'BATCH_RESULT') {
158
+ this.applyBatchResults(e.data.results);
159
+ }
160
+ };
161
+
162
+ this.worker.onerror = (error) => {
163
+ console.error('Erreur Worker:', error);
164
+ this.workerEnabled = false;
165
+ };
166
+ } catch (error) {
167
+ console.error('Impossible de créer le Worker:', error);
168
+ this.workerEnabled = false;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Applique les résultats calculés par le Worker
174
+ * @private
175
+ */
176
+ applyBatchResults(results) {
177
+ const toDelete = [];
178
+
179
+ for (let result of results) {
180
+ const anim = this.animations.get(result.id);
181
+ if (!anim) continue;
182
+
183
+ if (result.status === 'delaying') {
184
+ // Mettre à jour l'état de délai
185
+ Object.assign(anim, result.animation);
186
+ continue;
187
+ }
188
+
189
+ // Mettre à jour les propriétés du composant
190
+ if (result.values) {
191
+ for (let prop in result.values) {
192
+ anim.component[prop] = result.values[prop];
193
+ }
194
+ }
195
+
196
+ // Callback onUpdate
197
+ if (anim.onUpdate) {
198
+ anim.onUpdate(result.actualProgress);
199
+ }
200
+
201
+ // Gérer les états
202
+ if (result.status === 'yoyo' || result.status === 'loop') {
203
+ Object.assign(anim, result.animation);
204
+ } else if (result.status === 'complete') {
205
+ if (anim.onComplete) {
206
+ anim.onComplete();
207
+ }
208
+ toDelete.push(result.id);
209
+ }
210
+ }
211
+
212
+ // Nettoyer les animations terminées
213
+ toDelete.forEach(id => this.stop(id));
214
+ }
215
+
216
+ /**
217
+ * Crée un OffscreenCanvas pour pré-rendre un composant
218
+ * @param {string} cacheKey - Clé de cache
219
+ * @param {number} width - Largeur
220
+ * @param {number} height - Hauteur
221
+ * @param {Function} renderFn - Fonction de rendu
222
+ */
223
+ createOffscreenCache(cacheKey, width, height, renderFn) {
224
+ if (!window.OffscreenCanvas) {
225
+ console.warn('OffscreenCanvas non supporté');
226
+ return null;
227
+ }
228
+
229
+ const offscreen = new OffscreenCanvas(width, height);
230
+ const ctx = offscreen.getContext('2d');
231
+
232
+ renderFn(ctx);
233
+
234
+ this.offscreenCache.set(cacheKey, offscreen);
235
+ return offscreen;
236
+ }
237
+
238
+ /**
239
+ * Récupère un OffscreenCanvas du cache
240
+ * @param {string} cacheKey - Clé de cache
241
+ */
242
+ getOffscreenCache(cacheKey) {
243
+ return this.offscreenCache.get(cacheKey);
244
+ }
245
+
246
+ /**
247
+ * Nettoie le cache OffscreenCanvas
248
+ * @param {string} cacheKey - Clé de cache (optionnel)
249
+ */
250
+ clearOffscreenCache(cacheKey = null) {
251
+ if (cacheKey) {
252
+ this.offscreenCache.delete(cacheKey);
253
+ } else {
254
+ this.offscreenCache.clear();
255
+ }
13
256
  }
14
257
 
15
258
  /**
@@ -283,11 +526,69 @@ class AnimationEngine {
283
526
  }
284
527
 
285
528
  /**
286
- * Met à jour toutes les animations
529
+ * Met à jour toutes les animations (avec Worker et batching)
287
530
  * @private
288
531
  */
289
532
  update() {
290
533
  const currentTime = performance.now();
534
+
535
+ if (this.workerEnabled && this.worker) {
536
+ // Utiliser le Worker pour les calculs
537
+ this.updateWithWorker(currentTime);
538
+ } else {
539
+ // Fallback sur le thread principal
540
+ this.updateOnMainThread(currentTime);
541
+ }
542
+
543
+ // Continuer l'animation
544
+ if (this.animations.size > 0) {
545
+ this.animationFrameId = requestAnimationFrame(() => this.update());
546
+ } else {
547
+ this.isRunning = false;
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Met à jour avec le Worker (batching automatique)
553
+ * @private
554
+ */
555
+ updateWithWorker(currentTime) {
556
+ const animationsArray = [];
557
+
558
+ for (let [id, anim] of this.animations) {
559
+ // Préparer les données pour le Worker (sans les callbacks et le composant)
560
+ animationsArray.push({
561
+ id: anim.id,
562
+ from: anim.from,
563
+ to: anim.to,
564
+ duration: anim.duration,
565
+ easing: anim.easing,
566
+ delay: anim.delay,
567
+ loop: anim.loop,
568
+ yoyo: anim.yoyo,
569
+ startTime: anim.startTime,
570
+ delayStartTime: anim.delayStartTime,
571
+ isDelaying: anim.isDelaying,
572
+ isReversed: anim.isReversed
573
+ });
574
+ }
575
+
576
+ if (animationsArray.length > 0) {
577
+ this.worker.postMessage({
578
+ type: 'CALCULATE_BATCH',
579
+ data: {
580
+ animations: animationsArray,
581
+ currentTime: currentTime
582
+ }
583
+ });
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Met à jour sur le thread principal (fallback)
589
+ * @private
590
+ */
591
+ updateOnMainThread(currentTime) {
291
592
  const toDelete = [];
292
593
 
293
594
  for (let [id, anim] of this.animations) {
@@ -354,13 +655,6 @@ class AnimationEngine {
354
655
 
355
656
  // Nettoyer les animations terminées
356
657
  toDelete.forEach(id => this.stop(id));
357
-
358
- // Continuer l'animation
359
- if (this.animations.size > 0) {
360
- this.animationFrameId = requestAnimationFrame(() => this.update());
361
- } else {
362
- this.isRunning = false;
363
- }
364
658
  }
365
659
 
366
660
  /**
@@ -422,6 +716,18 @@ class AnimationEngine {
422
716
  cancelAnimationFrame(this.animationFrameId);
423
717
  this.animationFrameId = null;
424
718
  }
719
+ this.clearOffscreenCache();
720
+ }
721
+
722
+ /**
723
+ * Nettoie le Worker
724
+ */
725
+ destroy() {
726
+ this.clear();
727
+ if (this.worker) {
728
+ this.worker.terminate();
729
+ this.worker = null;
730
+ }
425
731
  }
426
732
  }
427
733