canvasframework 0.5.18 → 0.5.20
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/README.md +30 -0
- package/components/Accordion.js +265 -0
- package/components/AndroidDatePickerDialog.js +406 -0
- package/components/AppBar.js +398 -0
- package/components/AudioPlayer.js +611 -0
- package/components/Avatar.js +202 -0
- package/components/Banner.js +342 -0
- package/components/BottomNavigationBar.js +433 -0
- package/components/BottomSheet.js +234 -0
- package/components/Button.js +358 -0
- package/components/Camera.js +644 -0
- package/components/Card.js +193 -0
- package/components/Chart.js +700 -0
- package/components/Checkbox.js +166 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +327 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +298 -0
- package/components/Dialog.js +337 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +276 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/FloatedCamera.js +644 -0
- package/components/IOSDatePickerWheel.js +430 -0
- package/components/ImageCarousel.js +219 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +831 -0
- package/components/InputDatalist.js +723 -0
- package/components/InputTags.js +624 -0
- package/components/List.js +95 -0
- package/components/ListItem.js +269 -0
- package/components/Modal.js +364 -0
- package/components/MorphingFAB.js +428 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/PasswordInput.js +462 -0
- package/components/ProgressBar.js +88 -0
- package/components/QRCodeReader.js +539 -0
- package/components/RadioButton.js +151 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +357 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/SliverAppBar.js +139 -0
- package/components/Snackbar.js +243 -0
- package/components/SpeedDialFAB.js +397 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +327 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +423 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +151 -0
- package/components/TimePicker.js +934 -0
- package/components/Toast.js +236 -0
- package/components/TreeView.js +420 -0
- package/components/Video.js +397 -0
- package/components/View.js +140 -0
- package/components/VirtualList.js +120 -0
- package/core/CanvasFramework.js +3045 -0
- package/core/Component.js +243 -0
- package/core/ThemeManager.js +358 -0
- package/core/UIBuilder.js +267 -0
- package/core/WebGLCanvasAdapter.js +782 -0
- package/features/Column.js +43 -0
- package/features/Grid.js +47 -0
- package/features/LayoutComponent.js +43 -0
- package/features/OpenStreetMap.js +310 -0
- package/features/Positioned.js +33 -0
- package/features/PullToRefresh.js +328 -0
- package/features/Row.js +40 -0
- package/features/SignaturePad.js +257 -0
- package/features/Skeleton.js +193 -0
- package/features/Stack.js +21 -0
- package/index.js +119 -0
- package/manager/AccessibilityManager.js +107 -0
- package/manager/ErrorHandler.js +59 -0
- package/manager/FeatureFlags.js +60 -0
- package/manager/MemoryManager.js +107 -0
- package/manager/PerformanceMonitor.js +84 -0
- package/manager/SecurityManager.js +54 -0
- package/package.json +22 -16
- package/utils/AnimationEngine.js +734 -0
- package/utils/CryptoManager.js +303 -0
- package/utils/DataStore.js +403 -0
- package/utils/DevTools.js +1618 -0
- package/utils/DevToolsConsole.js +201 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FirebaseAuth.js +653 -0
- package/utils/FirebaseCore.js +246 -0
- package/utils/FirebaseFirestore.js +581 -0
- package/utils/FirebaseFunctions.js +97 -0
- package/utils/FirebaseRealtimeDB.js +498 -0
- package/utils/FirebaseStorage.js +612 -0
- package/utils/FormValidator.js +355 -0
- package/utils/GeoLocationService.js +62 -0
- package/utils/I18n.js +207 -0
- package/utils/IndexedDBManager.js +273 -0
- package/utils/InspectionOverlay.js +308 -0
- package/utils/NotificationManager.js +60 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/PayPalPayment.js +678 -0
- package/utils/QueryBuilder.js +478 -0
- package/utils/SafeArea.js +64 -0
- package/utils/SecureStorage.js +289 -0
- package/utils/StateManager.js +207 -0
- package/utils/StripePayment.js +552 -0
- package/utils/WebSocketClient.js +66 -0
- package/dist/canvasframework.js +0 -2
- package/dist/canvasframework.js.LICENSE.txt +0 -1
|
@@ -0,0 +1,3045 @@
|
|
|
1
|
+
import Button from '../components/Button.js';
|
|
2
|
+
import SegmentedControl from '../components/SegmentedControl.js';
|
|
3
|
+
import Input from '../components/Input.js';
|
|
4
|
+
import Slider from '../components/Slider.js';
|
|
5
|
+
import Text from '../components/Text.js';
|
|
6
|
+
import View from '../components/View.js';
|
|
7
|
+
import Card from '../components/Card.js';
|
|
8
|
+
import FAB from '../components/FAB.js';
|
|
9
|
+
import SpeedDialFAB from '../components/SpeedDialFAB.js';
|
|
10
|
+
import MorphingFAB from '../components/MorphingFAB.js';
|
|
11
|
+
import CircularProgress from '../components/CircularProgress.js';
|
|
12
|
+
import ImageComponent from '../components/ImageComponent.js';
|
|
13
|
+
import DatePicker from '../components/DatePicker.js';
|
|
14
|
+
import IOSDatePickerWheel from '../components/IOSDatePickerWheel.js';
|
|
15
|
+
import AndroidDatePickerDialog from '../components/AndroidDatePickerDialog.js';
|
|
16
|
+
import Avatar from '../components/Avatar.js';
|
|
17
|
+
import Snackbar from '../components/Snackbar.js';
|
|
18
|
+
import BottomNavigationBar from '../components/BottomNavigationBar.js';
|
|
19
|
+
import Video from '../components/Video.js';
|
|
20
|
+
import Modal from '../components/Modal.js';
|
|
21
|
+
import Drawer from '../components/Drawer.js';
|
|
22
|
+
import AppBar from '../components/AppBar.js';
|
|
23
|
+
import Chip from '../components/Chip.js';
|
|
24
|
+
import Stepper from '../components/Stepper.js';
|
|
25
|
+
import Accordion from '../components/Accordion.js';
|
|
26
|
+
import Tabs from '../components/Tabs.js';
|
|
27
|
+
import Switch from '../components/Switch.js';
|
|
28
|
+
import SwipeableListItem from '../components/SwipeableListItem.js';
|
|
29
|
+
import ListItem from '../components/ListItem.js';
|
|
30
|
+
import List from '../components/List.js';
|
|
31
|
+
import VirtualList from '../components/VirtualList.js';
|
|
32
|
+
import BottomSheet from '../components/BottomSheet.js';
|
|
33
|
+
import ProgressBar from '../components/ProgressBar.js';
|
|
34
|
+
import RadioButton from '../components/RadioButton.js';
|
|
35
|
+
import Dialog from '../components/Dialog.js';
|
|
36
|
+
import ContextMenu from '../components/ContextMenu.js';
|
|
37
|
+
import Checkbox from '../components/Checkbox.js';
|
|
38
|
+
import Toast from '../components/Toast.js';
|
|
39
|
+
import NumberInput from '../components/NumberInput.js';
|
|
40
|
+
import TextField from '../components/TextField.js';
|
|
41
|
+
import SelectDialog from '../components/SelectDialog.js';
|
|
42
|
+
import Select from '../components/Select.js';
|
|
43
|
+
import MultiSelectDialog from '../components/MultiSelectDialog.js';
|
|
44
|
+
import Divider from '../components/Divider.js';
|
|
45
|
+
import FileUpload from '../components/FileUpload.js';
|
|
46
|
+
import Table from '../components/Table.js';
|
|
47
|
+
import TreeView from '../components/TreeView.js';
|
|
48
|
+
import SearchInput from '../components/SearchInput.js';
|
|
49
|
+
import ImageCarousel from '../components/ImageCarousel.js';
|
|
50
|
+
import PasswordInput from '../components/PasswordInput.js';
|
|
51
|
+
import InputTags from '../components/InputTags.js';
|
|
52
|
+
import InputDatalist from '../components/InputDatalist.js';
|
|
53
|
+
import Banner from '../components/Banner.js';
|
|
54
|
+
import Chart from '../components/Chart.js';
|
|
55
|
+
import SliverAppBar from '../components/SliverAppBar.js';
|
|
56
|
+
import AudioPlayer from '../components/AudioPlayer.js';
|
|
57
|
+
import Camera from '../components/Camera.js';
|
|
58
|
+
import FloatedCamera from '../components/FloatedCamera.js';
|
|
59
|
+
import TimePicker from '../components/TimePicker.js';
|
|
60
|
+
import QRCodeReader from '../components/QRCodeReader.js';
|
|
61
|
+
|
|
62
|
+
// Utils
|
|
63
|
+
import SafeArea from '../utils/SafeArea.js';
|
|
64
|
+
import StateManager from '../utils/StateManager.js';
|
|
65
|
+
import I18n from '../utils/I18n.js';
|
|
66
|
+
import SecureStorage from '../utils/SecureStorage.js';
|
|
67
|
+
import FormValidator from '../utils/FormValidator.js';
|
|
68
|
+
import DataStore from '../utils/DataStore.js';
|
|
69
|
+
import EventBus from '../utils/EventBus.js';
|
|
70
|
+
import IndexedDBManager from '../utils/IndexedDBManager.js';
|
|
71
|
+
import QueryBuilder from '../utils/QueryBuilder.js';
|
|
72
|
+
import OfflineSyncManager from '../utils/OfflineSyncManager.js';
|
|
73
|
+
import FetchClient from '../utils/FetchClient.js';
|
|
74
|
+
import GeoLocationService from '../utils/GeoLocationService.js';
|
|
75
|
+
import WebSocketClient from '../utils/WebSocketClient.js';
|
|
76
|
+
import AnimationEngine from '../utils/AnimationEngine.js';
|
|
77
|
+
import CryptoManager from '../utils/CryptoManager.js';
|
|
78
|
+
import NotificationManager from '../utils/NotificationManager.js';
|
|
79
|
+
|
|
80
|
+
// DevTools
|
|
81
|
+
import DevTools from '../utils/DevTools.js';
|
|
82
|
+
import InspectionOverlay from '../utils/InspectionOverlay.js';
|
|
83
|
+
import DevToolsConsole from '../utils/DevToolsConsole.js';
|
|
84
|
+
|
|
85
|
+
// Features
|
|
86
|
+
import PullToRefresh from '../features/PullToRefresh.js';
|
|
87
|
+
import Skeleton from '../features/Skeleton.js';
|
|
88
|
+
import SignaturePad from '../features/SignaturePad.js';
|
|
89
|
+
import OpenStreetMap from '../features/OpenStreetMap.js';
|
|
90
|
+
import LayoutComponent from '../features/LayoutComponent.js';
|
|
91
|
+
import Grid from '../features/Grid.js';
|
|
92
|
+
import Row from '../features/Row.js';
|
|
93
|
+
import Column from '../features/Column.js';
|
|
94
|
+
import Positioned from '../features/Positioned.js';
|
|
95
|
+
import Stack from '../features/Stack.js';
|
|
96
|
+
|
|
97
|
+
// Manager
|
|
98
|
+
import ErrorHandler from '../manager/ErrorHandler.js';
|
|
99
|
+
import PerformanceMonitor from '../manager/PerformanceMonitor.js';
|
|
100
|
+
import AccessibilityManager from '../manager/AccessibilityManager.js';
|
|
101
|
+
import MemoryManager from '../manager/MemoryManager.js';
|
|
102
|
+
import SecurityManager from '../manager/SecurityManager.js';
|
|
103
|
+
import FeatureFlags from '../manager/FeatureFlags.js';
|
|
104
|
+
|
|
105
|
+
// WebGL Adapter
|
|
106
|
+
import WebGLCanvasAdapter from './WebGLCanvasAdapter.js';
|
|
107
|
+
import ui, {
|
|
108
|
+
createRef
|
|
109
|
+
} from './UIBuilder.js';
|
|
110
|
+
import ThemeManager from './ThemeManager.js';
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
const FIXED_COMPONENT_TYPES = new Set([
|
|
114
|
+
AppBar,
|
|
115
|
+
BottomNavigationBar,
|
|
116
|
+
Drawer,
|
|
117
|
+
Dialog,
|
|
118
|
+
Modal,
|
|
119
|
+
Tabs,
|
|
120
|
+
FAB,
|
|
121
|
+
Toast,
|
|
122
|
+
Camera,
|
|
123
|
+
QRCodeReader,
|
|
124
|
+
Banner,
|
|
125
|
+
SliverAppBar,
|
|
126
|
+
BottomSheet,
|
|
127
|
+
ContextMenu,
|
|
128
|
+
OpenStreetMap,
|
|
129
|
+
SelectDialog
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Framework principal pour créer des interfaces utilisateur basées sur Canvas
|
|
134
|
+
* @class
|
|
135
|
+
* @property {HTMLCanvasElement} canvas - Élément canvas HTML
|
|
136
|
+
* @property {CanvasRenderingContext2D} ctx - Contexte 2D du canvas
|
|
137
|
+
* @property {number} width - Largeur du canvas
|
|
138
|
+
* @property {number} height - Hauteur du canvas
|
|
139
|
+
* @property {number} dpr - Device Pixel Ratio
|
|
140
|
+
* @property {string} platform - Plateforme détectée ('material' ou 'cupertino')
|
|
141
|
+
* @property {Component[]} components - Liste des composants
|
|
142
|
+
* @property {Map} routes - Routes de navigation
|
|
143
|
+
* @property {string} currentRoute - Route actuelle
|
|
144
|
+
* @property {Object} state - État global
|
|
145
|
+
* @property {boolean} isDragging - Indique si un drag est en cours
|
|
146
|
+
* @property {number} lastTouchY - Dernière position Y du touch
|
|
147
|
+
* @property {number} scrollOffset - Offset de défilement
|
|
148
|
+
* @property {number} scrollVelocity - Vitesse de défilement
|
|
149
|
+
* @property {number} scrollFriction - Friction du défilement
|
|
150
|
+
*/
|
|
151
|
+
class CanvasFramework {
|
|
152
|
+
/**
|
|
153
|
+
* Crée une instance de CanvasFramework
|
|
154
|
+
* @param {string} canvasId - ID de l'élément canvas
|
|
155
|
+
*/
|
|
156
|
+
constructor(canvasId, options = {}) {
|
|
157
|
+
// ✅ AJOUTER: Démarrer le chronomètre
|
|
158
|
+
const startTime = performance.now();
|
|
159
|
+
|
|
160
|
+
this.metrics = {
|
|
161
|
+
initTime: 0,
|
|
162
|
+
firstRenderTime: null,
|
|
163
|
+
firstInteractionTime: null,
|
|
164
|
+
totalStartupTime: null
|
|
165
|
+
};
|
|
166
|
+
this._firstRenderDone = false;
|
|
167
|
+
this._startupStartTime = startTime;
|
|
168
|
+
|
|
169
|
+
// ✅ Créer automatiquement le canvas
|
|
170
|
+
this.canvas = document.createElement('canvas');
|
|
171
|
+
this.canvas.id = canvasId || `canvas-${Date.now()}`;
|
|
172
|
+
this.canvas.style.display = 'block';
|
|
173
|
+
this.canvas.style.touchAction = 'none';
|
|
174
|
+
this.canvas.style.userSelect = 'none';
|
|
175
|
+
document.body.appendChild(this.canvas);
|
|
176
|
+
|
|
177
|
+
// NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
|
|
178
|
+
this.useWebGL = options.useWebGL ?? false; // utilise la valeur si fournie, sinon false
|
|
179
|
+
|
|
180
|
+
// Initialiser le contexte approprié
|
|
181
|
+
if (this.useWebGL) {
|
|
182
|
+
try {
|
|
183
|
+
this.ctx = new WebGLCanvasAdapter(this.canvas, {
|
|
184
|
+
dpr: this.dpr,
|
|
185
|
+
alpha: false
|
|
186
|
+
});
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.warn("Échec de l’initialisation WebGLCanvasAdapter → fallback Canvas 2D", err);
|
|
189
|
+
this.ctx = this.canvas.getContext('2d', {
|
|
190
|
+
alpha: false,
|
|
191
|
+
desynchronized: true,
|
|
192
|
+
willReadFrequently: false
|
|
193
|
+
});
|
|
194
|
+
this.useWebGL = false;
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
this.ctx = this.canvas.getContext('2d', {
|
|
198
|
+
alpha: false,
|
|
199
|
+
desynchronized: true,
|
|
200
|
+
willReadFrequently: false
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
//this.ctx.scale(this.dpr, this.dpr);
|
|
204
|
+
|
|
205
|
+
this.backgroundColor = options.backgroundColor || '#f5f5f5'; // Blanc par défaut
|
|
206
|
+
|
|
207
|
+
this.width = window.innerWidth;
|
|
208
|
+
this.height = window.innerHeight;
|
|
209
|
+
|
|
210
|
+
this.dpr = window.devicePixelRatio || 1;
|
|
211
|
+
|
|
212
|
+
// ✅ OPTIMISATION OPTION 2: Configuration des optimisations
|
|
213
|
+
this.optimizations = {
|
|
214
|
+
enabled: options.optimizations !== false, // Activé par défaut
|
|
215
|
+
useDoubleBuffering: true,
|
|
216
|
+
useCaching: true,
|
|
217
|
+
useBatchDrawing: true,
|
|
218
|
+
useSpatialPartitioning: false, // Désactivé par défaut (à activer si beaucoup de composants)
|
|
219
|
+
useImageDataOptimization: true
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// ✅ OPTIMISATION OPTION 2: Cache pour éviter les changements d'état inutiles
|
|
223
|
+
this._stateCache = {
|
|
224
|
+
fillStyle: null,
|
|
225
|
+
strokeStyle: null,
|
|
226
|
+
font: null,
|
|
227
|
+
textAlign: null,
|
|
228
|
+
textBaseline: null,
|
|
229
|
+
lineWidth: null,
|
|
230
|
+
globalAlpha: 1
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// ✅ OPTIMISATION OPTION 2: Cache des images/textes
|
|
234
|
+
this.imageCache = new Map();
|
|
235
|
+
this.textCache = new Map();
|
|
236
|
+
|
|
237
|
+
// ✅ OPTIMISATION OPTION 2: Double buffering
|
|
238
|
+
this._doubleBuffer = null;
|
|
239
|
+
this._bufferCtx = null;
|
|
240
|
+
if (this.optimizations.useDoubleBuffering) {
|
|
241
|
+
this._initDoubleBuffer();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Scroll Worker
|
|
245
|
+
this.scrollWorker = this.createScrollWorker();
|
|
246
|
+
this.scrollWorker.onmessage = this.handleScrollWorkerMessage.bind(this);
|
|
247
|
+
|
|
248
|
+
this.splashOptions = {
|
|
249
|
+
enabled: options.splash?.enabled === true, // false par défaut
|
|
250
|
+
duration: options.splash?.duration || 700,
|
|
251
|
+
fadeOutDuration: options.splash?.fadeOutDuration || 500,
|
|
252
|
+
backgroundColor: options.splash?.backgroundColor || ['#667eea', '#764ba2'], // Gradient ou couleur unie
|
|
253
|
+
spinnerColor: options.splash?.spinnerColor || 'white',
|
|
254
|
+
spinnerBackground: options.splash?.spinnerBackground || 'rgba(255, 255, 255, 0.3)',
|
|
255
|
+
textColor: options.splash?.textColor || 'white',
|
|
256
|
+
text: options.splash?.text || 'Chargement...',
|
|
257
|
+
textSize: options.splash?.textSize || 20,
|
|
258
|
+
textFont: options.splash?.textFont || 'Arial',
|
|
259
|
+
progressBarColor: options.splash?.progressBarColor || 'white',
|
|
260
|
+
progressBarBackground: options.splash?.progressBarBackground || 'rgba(255, 255, 255, 0.3)',
|
|
261
|
+
showProgressBar: options.splash?.showProgressBar !== false, // true par défaut
|
|
262
|
+
logo: options.splash?.logo || null, // URL d'une image (optionnel)
|
|
263
|
+
logoWidth: options.splash?.logoWidth || 100,
|
|
264
|
+
logoHeight: options.splash?.logoHeight || 100
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// ✅ MODIFIER : Vérifier si le splash est activé
|
|
268
|
+
if (this.splashOptions.enabled) {
|
|
269
|
+
this.showSplashScreen();
|
|
270
|
+
} else {
|
|
271
|
+
this._splashFinished = true; // Passer directement au rendu
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.platform = this.detectPlatform();
|
|
275
|
+
setTimeout(() => {
|
|
276
|
+
this.initScrollWorker();
|
|
277
|
+
}, 100);
|
|
278
|
+
// État actuel + préférence
|
|
279
|
+
this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
|
|
280
|
+
this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
|
|
281
|
+
|
|
282
|
+
// Applique le thème initial
|
|
283
|
+
this.setupSystemThemeListener();
|
|
284
|
+
|
|
285
|
+
// Récupère override utilisateur
|
|
286
|
+
const savedOverride = localStorage.getItem('themeOverride');
|
|
287
|
+
if (savedOverride && ['light', 'dark'].includes(savedOverride)) {
|
|
288
|
+
this.userThemeOverride = savedOverride;
|
|
289
|
+
this.themeMode = savedOverride;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this.components = [];
|
|
293
|
+
// ✅ AJOUTER ICI :
|
|
294
|
+
this._cachedMaxScroll = 0;
|
|
295
|
+
this._maxScrollDirty = true;
|
|
296
|
+
this.resizeTimeout = null;
|
|
297
|
+
|
|
298
|
+
//this.applyThemeFromSystem();
|
|
299
|
+
this.state = {};
|
|
300
|
+
|
|
301
|
+
// Calcule FPS
|
|
302
|
+
this.fps = 0;
|
|
303
|
+
this._frames = 0;
|
|
304
|
+
this._lastFpsTime = performance.now();
|
|
305
|
+
this.showFps = options.showFps || false; // false par défaut
|
|
306
|
+
this.debbug = options.debug || false; // false par défaut (et correction de la faute de frappe)
|
|
307
|
+
|
|
308
|
+
// Worker pour multithreading Canvas Worker
|
|
309
|
+
this.worker = this.createCanvasWorker();
|
|
310
|
+
this.worker.onmessage = this.handleWorkerMessage.bind(this);
|
|
311
|
+
this.worker.postMessage({
|
|
312
|
+
type: 'INIT',
|
|
313
|
+
payload: {
|
|
314
|
+
components: []
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Logic Worker
|
|
319
|
+
this.logicWorker = this.createLogicWorker();
|
|
320
|
+
this.logicWorker.onmessage = this.handleLogicWorkerMessage.bind(this);
|
|
321
|
+
this.logicWorkerState = {};
|
|
322
|
+
this.logicWorker.postMessage({
|
|
323
|
+
type: 'SET_STATE',
|
|
324
|
+
payload: this.state
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Envoyer l'état initial au worker
|
|
328
|
+
this.logicWorker.postMessage({
|
|
329
|
+
type: 'SET_STATE',
|
|
330
|
+
payload: this.state
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Gestion des événements
|
|
334
|
+
this.isDragging = false;
|
|
335
|
+
this.lastTouchY = 0;
|
|
336
|
+
this.scrollOffset = 0;
|
|
337
|
+
this.scrollVelocity = 0;
|
|
338
|
+
this.scrollFriction = 0.95;
|
|
339
|
+
|
|
340
|
+
// Optimisation
|
|
341
|
+
this.dirtyComponents = new Set();
|
|
342
|
+
this.optimizationEnabled = this.optimizations.enabled;
|
|
343
|
+
|
|
344
|
+
// AJOUTER CETTE LIGNE
|
|
345
|
+
this.animator = new AnimationEngine();
|
|
346
|
+
|
|
347
|
+
// ===== NOUVEAU SYSTÈME DE ROUTING =====
|
|
348
|
+
this.routes = new Map();
|
|
349
|
+
this.currentRoute = '/';
|
|
350
|
+
this.currentParams = {};
|
|
351
|
+
this.currentQuery = {};
|
|
352
|
+
this.history = [];
|
|
353
|
+
this.historyIndex = -1;
|
|
354
|
+
|
|
355
|
+
// Animation de transition
|
|
356
|
+
this.transitionState = {
|
|
357
|
+
isTransitioning: false,
|
|
358
|
+
progress: 0,
|
|
359
|
+
duration: 300,
|
|
360
|
+
type: 'slide', // 'slide', 'fade', 'none'
|
|
361
|
+
direction: 'forward', // 'forward', 'back'
|
|
362
|
+
oldComponents: [],
|
|
363
|
+
newComponents: []
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
this.setupCanvas();
|
|
367
|
+
|
|
368
|
+
// ✅ OPTIMISATION OPTION 5: Désactiver l'antialiasing pour meilleures performances
|
|
369
|
+
this._disableImageSmoothing();
|
|
370
|
+
|
|
371
|
+
this.setupEventListeners();
|
|
372
|
+
this.setupHistoryListener();
|
|
373
|
+
|
|
374
|
+
this.startRenderLoop();
|
|
375
|
+
|
|
376
|
+
this.devTools = new DevTools(this);
|
|
377
|
+
this.inspectionOverlay = new InspectionOverlay(this);
|
|
378
|
+
|
|
379
|
+
// MODIFICATION: Vérifier explicitement l'option enableDevTools
|
|
380
|
+
const shouldEnableDevTools = options.enableDevTools === true;
|
|
381
|
+
|
|
382
|
+
if (shouldEnableDevTools) {
|
|
383
|
+
this.enableDevTools();
|
|
384
|
+
console.log('DevTools enabled. Press Ctrl+Shift+D to toggle.');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Initialiser le ThemeManager
|
|
388
|
+
this.themeManager = new ThemeManager(this, {
|
|
389
|
+
lightTheme: options.lightTheme,
|
|
390
|
+
darkTheme: options.darkTheme,
|
|
391
|
+
storageKey: options.themeStorageKey || 'app-theme-mode'
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Raccourci pour accéder au thème actuel
|
|
395
|
+
this.theme = this.themeManager.getTheme();
|
|
396
|
+
|
|
397
|
+
// ✅ AJOUTER: Mesurer le temps d'init
|
|
398
|
+
const initTime = performance.now() - startTime;
|
|
399
|
+
|
|
400
|
+
// ✅ AJOUTER: Stocker les métriques
|
|
401
|
+
this.metrics = {
|
|
402
|
+
initTime: initTime,
|
|
403
|
+
firstRenderTime: null,
|
|
404
|
+
firstInteractionTime: null,
|
|
405
|
+
totalStartupTime: null
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// ✅ AJOUTER: Logger si debug
|
|
409
|
+
if (options.debug || options.showMetrics) {
|
|
410
|
+
console.log(`⚡ Framework initialisé en ${initTime.toFixed(2)}ms`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ✅ AJOUTER: Marquer le premier rendu
|
|
414
|
+
this._firstRenderDone = false;
|
|
415
|
+
this._startupStartTime = startTime;
|
|
416
|
+
|
|
417
|
+
// ✅ OPTIMISATION OPTION 5: Partition spatiale pour le culling (optionnel)
|
|
418
|
+
if (this.optimizations.useSpatialPartitioning) {
|
|
419
|
+
this._initSpatialPartitioning();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Crée un élément DOM temporaire, l'ajoute au body, exécute une callback, puis le supprime
|
|
425
|
+
* @param {string} tagName - 'input', 'select', 'textarea', etc.
|
|
426
|
+
* @param {Object} props - propriétés de base (type, value, accept, placeholder...)
|
|
427
|
+
* @param {Function} onResult - callback quand l'élément change ou blur (reçoit l'élément)
|
|
428
|
+
* @param {Object} [position=null] - {left, top, width, height} en pixels
|
|
429
|
+
* @param {Object} [attributes={}] - attributs supplémentaires (id, className, data-*, etc.)
|
|
430
|
+
* @returns {HTMLElement} L'élément créé (avant suppression)
|
|
431
|
+
*/
|
|
432
|
+
createTemporaryDomElement(tagName, props = {}, onResult, position = null, attributes = {}) {
|
|
433
|
+
const el = document.createElement(tagName);
|
|
434
|
+
|
|
435
|
+
// Appliquer les propriétés de base
|
|
436
|
+
Object.assign(el, props);
|
|
437
|
+
|
|
438
|
+
// Appliquer les attributs personnalisés (id, class, data-*, etc.)
|
|
439
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
440
|
+
if (key === 'className') {
|
|
441
|
+
el.className = value; // className est spécial en JS
|
|
442
|
+
} else {
|
|
443
|
+
el.setAttribute(key, value);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Styles de base pour le rendre invisible
|
|
448
|
+
el.style.position = 'absolute';
|
|
449
|
+
el.style.opacity = '0';
|
|
450
|
+
el.style.zIndex = '9999';
|
|
451
|
+
|
|
452
|
+
// Positionnement optionnel
|
|
453
|
+
if (position) {
|
|
454
|
+
Object.assign(el.style, {
|
|
455
|
+
left: `${position.left}px`,
|
|
456
|
+
top: `${position.top}px`,
|
|
457
|
+
width: `${position.width}px`,
|
|
458
|
+
height: `${position.height}px`
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
document.body.appendChild(el);
|
|
463
|
+
|
|
464
|
+
const cleanup = () => {
|
|
465
|
+
el.remove();
|
|
466
|
+
document.removeEventListener('focusout', cleanup);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// Événements
|
|
470
|
+
el.addEventListener('change', (e) => {
|
|
471
|
+
onResult(e.target);
|
|
472
|
+
cleanup();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
el.addEventListener('blur', cleanup);
|
|
476
|
+
|
|
477
|
+
// Focus automatique pour les champs saisissables
|
|
478
|
+
if (['input', 'select', 'textarea'].includes(tagName.toLowerCase())) {
|
|
479
|
+
el.focus();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return el;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Crée le Worker pour le calcul du scroll
|
|
487
|
+
*/
|
|
488
|
+
createScrollWorker() {
|
|
489
|
+
const workerCode = `
|
|
490
|
+
let state = {
|
|
491
|
+
scrollOffset: 0,
|
|
492
|
+
scrollVelocity: 0,
|
|
493
|
+
scrollFriction: 0.95,
|
|
494
|
+
isDragging: false,
|
|
495
|
+
maxScroll: 0,
|
|
496
|
+
height: 0,
|
|
497
|
+
lastTouchY: 0,
|
|
498
|
+
components: []
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const FIXED_COMPONENT_TYPES = [
|
|
502
|
+
'AppBar', 'BottomNavigationBar', 'Drawer', 'Dialog', 'Modal',
|
|
503
|
+
'Tabs', 'FAB', 'Toast', 'Camera', 'QRCodeReader', 'Banner',
|
|
504
|
+
'SliverAppBar', 'BottomSheet', 'ContextMenu', 'OpenStreetMap', 'SelectDialog'
|
|
505
|
+
];
|
|
506
|
+
|
|
507
|
+
const calculateMaxScroll = () => {
|
|
508
|
+
let maxY = 0;
|
|
509
|
+
|
|
510
|
+
for (const comp of state.components) {
|
|
511
|
+
if (FIXED_COMPONENT_TYPES.includes(comp.type) || !comp.visible) continue;
|
|
512
|
+
const bottom = comp.y + comp.height;
|
|
513
|
+
if (bottom > maxY) maxY = bottom;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return Math.max(0, maxY - state.height + 50);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const updateInertia = () => {
|
|
520
|
+
if (Math.abs(state.scrollVelocity) > 0.1 && !state.isDragging) {
|
|
521
|
+
state.scrollOffset += state.scrollVelocity;
|
|
522
|
+
state.maxScroll = calculateMaxScroll();
|
|
523
|
+
state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
|
|
524
|
+
state.scrollVelocity *= state.scrollFriction;
|
|
525
|
+
} else {
|
|
526
|
+
state.scrollVelocity = 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
scrollOffset: state.scrollOffset,
|
|
531
|
+
scrollVelocity: state.scrollVelocity,
|
|
532
|
+
maxScroll: state.maxScroll
|
|
533
|
+
};
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const handleTouchMove = (deltaY) => {
|
|
537
|
+
if (state.isDragging) {
|
|
538
|
+
state.scrollOffset += deltaY;
|
|
539
|
+
state.maxScroll = calculateMaxScroll();
|
|
540
|
+
state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
|
|
541
|
+
state.scrollVelocity = deltaY;
|
|
542
|
+
return {
|
|
543
|
+
scrollOffset: state.scrollOffset,
|
|
544
|
+
scrollVelocity: state.scrollVelocity,
|
|
545
|
+
maxScroll: state.maxScroll
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
self.onmessage = (e) => {
|
|
552
|
+
const { type, payload } = e.data;
|
|
553
|
+
|
|
554
|
+
switch (type) {
|
|
555
|
+
case 'INIT':
|
|
556
|
+
state = {
|
|
557
|
+
...state,
|
|
558
|
+
...payload
|
|
559
|
+
};
|
|
560
|
+
state.maxScroll = calculateMaxScroll();
|
|
561
|
+
self.postMessage({
|
|
562
|
+
type: 'INITIALIZED',
|
|
563
|
+
payload: {
|
|
564
|
+
scrollOffset: state.scrollOffset,
|
|
565
|
+
maxScroll: state.maxScroll
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
break;
|
|
569
|
+
|
|
570
|
+
case 'UPDATE_COMPONENTS':
|
|
571
|
+
state.components = payload.components;
|
|
572
|
+
state.maxScroll = calculateMaxScroll();
|
|
573
|
+
self.postMessage({
|
|
574
|
+
type: 'MAX_SCROLL_UPDATED',
|
|
575
|
+
payload: { maxScroll: state.maxScroll }
|
|
576
|
+
});
|
|
577
|
+
break;
|
|
578
|
+
|
|
579
|
+
case 'UPDATE_DIMENSIONS':
|
|
580
|
+
state.height = payload.height;
|
|
581
|
+
state.maxScroll = calculateMaxScroll();
|
|
582
|
+
self.postMessage({
|
|
583
|
+
type: 'MAX_SCROLL_UPDATED',
|
|
584
|
+
payload: { maxScroll: state.maxScroll }
|
|
585
|
+
});
|
|
586
|
+
break;
|
|
587
|
+
|
|
588
|
+
case 'SET_DRAGGING':
|
|
589
|
+
state.isDragging = payload.isDragging;
|
|
590
|
+
if (!payload.isDragging) {
|
|
591
|
+
state.scrollVelocity = payload.lastVelocity || 0;
|
|
592
|
+
} else {
|
|
593
|
+
state.lastTouchY = payload.lastTouchY || 0;
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
|
|
597
|
+
case 'HANDLE_TOUCH_MOVE':
|
|
598
|
+
const result = handleTouchMove(payload.deltaY);
|
|
599
|
+
if (result) {
|
|
600
|
+
self.postMessage({
|
|
601
|
+
type: 'SCROLL_UPDATED',
|
|
602
|
+
payload: result
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
break;
|
|
606
|
+
|
|
607
|
+
case 'UPDATE_INERTIA':
|
|
608
|
+
const inertiaResult = updateInertia();
|
|
609
|
+
self.postMessage({
|
|
610
|
+
type: 'SCROLL_UPDATED',
|
|
611
|
+
payload: inertiaResult
|
|
612
|
+
});
|
|
613
|
+
break;
|
|
614
|
+
|
|
615
|
+
case 'SET_SCROLL_OFFSET':
|
|
616
|
+
state.scrollOffset = payload.scrollOffset;
|
|
617
|
+
state.maxScroll = calculateMaxScroll();
|
|
618
|
+
state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
|
|
619
|
+
self.postMessage({
|
|
620
|
+
type: 'SCROLL_UPDATED',
|
|
621
|
+
payload: {
|
|
622
|
+
scrollOffset: state.scrollOffset,
|
|
623
|
+
maxScroll: state.maxScroll
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
break;
|
|
627
|
+
|
|
628
|
+
case 'GET_STATE':
|
|
629
|
+
self.postMessage({
|
|
630
|
+
type: 'STATE',
|
|
631
|
+
payload: {
|
|
632
|
+
scrollOffset: state.scrollOffset,
|
|
633
|
+
scrollVelocity: state.scrollVelocity,
|
|
634
|
+
maxScroll: state.maxScroll,
|
|
635
|
+
isDragging: state.isDragging
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
`;
|
|
642
|
+
|
|
643
|
+
const blob = new Blob([workerCode], {
|
|
644
|
+
type: 'application/javascript'
|
|
645
|
+
});
|
|
646
|
+
return new Worker(URL.createObjectURL(blob));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Gère les messages du Scroll Worker
|
|
651
|
+
*/
|
|
652
|
+
handleScrollWorkerMessage(e) {
|
|
653
|
+
const {
|
|
654
|
+
type,
|
|
655
|
+
payload
|
|
656
|
+
} = e.data;
|
|
657
|
+
|
|
658
|
+
switch (type) {
|
|
659
|
+
case 'SCROLL_UPDATED':
|
|
660
|
+
this.scrollOffset = payload.scrollOffset;
|
|
661
|
+
this.scrollVelocity = payload.scrollVelocity;
|
|
662
|
+
// ✅ CORRECTION IMPORTANTE : Vider le cache dirty pendant le scroll
|
|
663
|
+
if (Math.abs(payload.scrollVelocity) > 0.5) {
|
|
664
|
+
this.dirtyComponents.clear();
|
|
665
|
+
}
|
|
666
|
+
// Mettre à jour le cache
|
|
667
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
668
|
+
this._maxScrollDirty = false;
|
|
669
|
+
|
|
670
|
+
// Marquer les composants comme sales pour mise à jour visuelle
|
|
671
|
+
if (Math.abs(payload.scrollVelocity) > 0) {
|
|
672
|
+
this.components.forEach(comp => {
|
|
673
|
+
if (!this.isFixedComponent(comp)) {
|
|
674
|
+
this.markComponentDirty(comp);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
break;
|
|
679
|
+
|
|
680
|
+
case 'MAX_SCROLL_UPDATED':
|
|
681
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
682
|
+
this._maxScrollDirty = false;
|
|
683
|
+
break;
|
|
684
|
+
|
|
685
|
+
case 'INITIALIZED':
|
|
686
|
+
this.scrollOffset = payload.scrollOffset;
|
|
687
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
688
|
+
this._maxScrollDirty = false;
|
|
689
|
+
break;
|
|
690
|
+
|
|
691
|
+
case 'STATE':
|
|
692
|
+
// Synchroniser l'état local
|
|
693
|
+
this.scrollOffset = payload.scrollOffset;
|
|
694
|
+
this.scrollVelocity = payload.scrollVelocity;
|
|
695
|
+
this.isDragging = payload.isDragging;
|
|
696
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
697
|
+
this._maxScrollDirty = false;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Initialise le Scroll Worker avec les données actuelles
|
|
704
|
+
*/
|
|
705
|
+
initScrollWorker() {
|
|
706
|
+
const componentsData = this.components.map(comp => ({
|
|
707
|
+
type: comp.constructor.name,
|
|
708
|
+
y: comp.y,
|
|
709
|
+
height: comp.height,
|
|
710
|
+
visible: comp.visible
|
|
711
|
+
}));
|
|
712
|
+
|
|
713
|
+
this.scrollWorker.postMessage({
|
|
714
|
+
type: 'INIT',
|
|
715
|
+
payload: {
|
|
716
|
+
scrollOffset: this.scrollOffset,
|
|
717
|
+
scrollVelocity: this.scrollVelocity,
|
|
718
|
+
scrollFriction: this.scrollFriction,
|
|
719
|
+
isDragging: this.isDragging,
|
|
720
|
+
height: this.height,
|
|
721
|
+
components: componentsData
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Met à jour les composants dans le Worker
|
|
728
|
+
*/
|
|
729
|
+
updateScrollWorkerComponents() {
|
|
730
|
+
const componentsData = this.components.map(comp => ({
|
|
731
|
+
type: comp.constructor.name,
|
|
732
|
+
y: comp.y,
|
|
733
|
+
height: comp.height,
|
|
734
|
+
visible: comp.visible
|
|
735
|
+
}));
|
|
736
|
+
|
|
737
|
+
this.scrollWorker.postMessage({
|
|
738
|
+
type: 'UPDATE_COMPONENTS',
|
|
739
|
+
payload: {
|
|
740
|
+
components: componentsData
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Initialise le double buffering pour éviter le flickering
|
|
747
|
+
* @private
|
|
748
|
+
*/
|
|
749
|
+
// Dans _initDoubleBuffer(), assurez-vous de bien configurer le contexte
|
|
750
|
+
_initDoubleBuffer() {
|
|
751
|
+
this._doubleBuffer = document.createElement('canvas');
|
|
752
|
+
this._bufferCtx = this._doubleBuffer.getContext('2d', {
|
|
753
|
+
alpha: false,
|
|
754
|
+
desynchronized: true
|
|
755
|
+
});
|
|
756
|
+
this._doubleBuffer.width = this.width * this.dpr;
|
|
757
|
+
this._doubleBuffer.height = this.height * this.dpr;
|
|
758
|
+
this._doubleBuffer.style.width = this.width + 'px';
|
|
759
|
+
this._doubleBuffer.style.height = this.height + 'px';
|
|
760
|
+
this._bufferCtx.scale(this.dpr, this.dpr);
|
|
761
|
+
this._disableImageSmoothing(this._bufferCtx);
|
|
762
|
+
|
|
763
|
+
// ✅ INITIALISER le background du buffer
|
|
764
|
+
this._bufferCtx.fillStyle = this.backgroundColor || '#ffffff';
|
|
765
|
+
this._bufferCtx.fillRect(0, 0, this.width, this.height);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Désactive l'antialiasing pour meilleures performances
|
|
771
|
+
* @private
|
|
772
|
+
* @param {CanvasRenderingContext2D} [ctx=this.ctx] - Contexte à configurer
|
|
773
|
+
*/
|
|
774
|
+
_disableImageSmoothing(ctx = this.ctx) {
|
|
775
|
+
ctx.imageSmoothingEnabled = false;
|
|
776
|
+
ctx.msImageSmoothingEnabled = false;
|
|
777
|
+
ctx.webkitImageSmoothingEnabled = false;
|
|
778
|
+
ctx.mozImageSmoothingEnabled = false;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Initialise le spatial partitioning pour le viewport culling
|
|
783
|
+
* @private
|
|
784
|
+
*/
|
|
785
|
+
_initSpatialPartitioning() {
|
|
786
|
+
// Simple grid spatial partitioning
|
|
787
|
+
this._spatialGrid = {
|
|
788
|
+
cellSize: 100,
|
|
789
|
+
grid: new Map(),
|
|
790
|
+
update: (components) => {
|
|
791
|
+
this._spatialGrid.grid.clear();
|
|
792
|
+
components.forEach(comp => {
|
|
793
|
+
if (!comp.visible) return;
|
|
794
|
+
|
|
795
|
+
const gridX = Math.floor(comp.x / this._spatialGrid.cellSize);
|
|
796
|
+
const gridY = Math.floor(comp.y / this._spatialGrid.cellSize);
|
|
797
|
+
const key = `${gridX},${gridY}`;
|
|
798
|
+
|
|
799
|
+
if (!this._spatialGrid.grid.has(key)) {
|
|
800
|
+
this._spatialGrid.grid.set(key, []);
|
|
801
|
+
}
|
|
802
|
+
this._spatialGrid.grid.get(key).push(comp);
|
|
803
|
+
});
|
|
804
|
+
},
|
|
805
|
+
getVisible: (viewportY) => {
|
|
806
|
+
const visible = [];
|
|
807
|
+
const startY = viewportY - 200; // Marge de 200px
|
|
808
|
+
const endY = viewportY + this.height + 200;
|
|
809
|
+
|
|
810
|
+
this._spatialGrid.grid.forEach((comps, key) => {
|
|
811
|
+
comps.forEach(comp => {
|
|
812
|
+
const compBottom = comp.y + comp.height;
|
|
813
|
+
if (compBottom >= startY && comp.y <= endY) {
|
|
814
|
+
visible.push(comp);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
return visible;
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* ✅ OPTIMISATION OPTION 2: Rendu optimisé de rectangles
|
|
825
|
+
* Évite les changements d'état inutiles
|
|
826
|
+
* @param {number} x - Position X
|
|
827
|
+
* @param {number} y - Position Y
|
|
828
|
+
* @param {number} w - Largeur
|
|
829
|
+
* @param {number} h - Hauteur
|
|
830
|
+
* @param {string} color - Couleur de remplissage
|
|
831
|
+
*/
|
|
832
|
+
fillRectOptimized(x, y, w, h, color) {
|
|
833
|
+
// Éviter les changements d'état inutiles
|
|
834
|
+
if (this._stateCache.fillStyle !== color) {
|
|
835
|
+
this.ctx.fillStyle = color;
|
|
836
|
+
this._stateCache.fillStyle = color;
|
|
837
|
+
}
|
|
838
|
+
this.ctx.fillRect(x, y, w, h);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* ✅ OPTIMISATION OPTION 2: Texte avec cache
|
|
843
|
+
* Cache le rendu du texte pour éviter de le redessiner à chaque frame
|
|
844
|
+
* @param {string} text - Texte à afficher
|
|
845
|
+
* @param {number} x - Position X
|
|
846
|
+
* @param {number} y - Position Y
|
|
847
|
+
* @param {string} font - Police CSS
|
|
848
|
+
* @param {string} color - Couleur du texte
|
|
849
|
+
*/
|
|
850
|
+
fillTextCached(text, x, y, font, color) {
|
|
851
|
+
const key = `${text}_${font}_${color}`;
|
|
852
|
+
|
|
853
|
+
if (!this.textCache.has(key)) {
|
|
854
|
+
// Rendu dans un canvas temporaire
|
|
855
|
+
const temp = document.createElement('canvas');
|
|
856
|
+
const tempCtx = temp.getContext('2d', {
|
|
857
|
+
alpha: false
|
|
858
|
+
});
|
|
859
|
+
tempCtx.font = font;
|
|
860
|
+
|
|
861
|
+
const metrics = tempCtx.measureText(text);
|
|
862
|
+
temp.width = Math.ceil(metrics.width);
|
|
863
|
+
temp.height = Math.ceil(parseInt(font) * 1.2);
|
|
864
|
+
|
|
865
|
+
tempCtx.font = font;
|
|
866
|
+
tempCtx.fillStyle = color;
|
|
867
|
+
tempCtx.textBaseline = 'top';
|
|
868
|
+
tempCtx.fillText(text, 0, 0);
|
|
869
|
+
|
|
870
|
+
this.textCache.set(key, {
|
|
871
|
+
canvas: temp,
|
|
872
|
+
width: temp.width,
|
|
873
|
+
height: temp.height,
|
|
874
|
+
baseline: parseInt(font)
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const cached = this.textCache.get(key);
|
|
879
|
+
this.ctx.drawImage(cached.canvas, x, y - cached.baseline);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* ✅ OPTIMISATION OPTION 5: Rendu batché pour plusieurs rectangles
|
|
884
|
+
* Regroupe les rectangles par couleur pour réduire les appels draw
|
|
885
|
+
* @param {Array} rects - Tableau d'objets {x, y, width, height, color}
|
|
886
|
+
*/
|
|
887
|
+
batchRect(rects) {
|
|
888
|
+
if (!rects || rects.length === 0) return;
|
|
889
|
+
|
|
890
|
+
// Regrouper par couleur
|
|
891
|
+
const batches = new Map();
|
|
892
|
+
|
|
893
|
+
rects.forEach(rect => {
|
|
894
|
+
if (!batches.has(rect.color)) {
|
|
895
|
+
batches.set(rect.color, []);
|
|
896
|
+
}
|
|
897
|
+
batches.get(rect.color).push(rect);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// Dessiner par batch
|
|
901
|
+
batches.forEach((batchRects, color) => {
|
|
902
|
+
this.ctx.fillStyle = color;
|
|
903
|
+
|
|
904
|
+
// Utiliser un seul path pour tous les rectangles de même couleur
|
|
905
|
+
this.ctx.beginPath();
|
|
906
|
+
batchRects.forEach(rect => {
|
|
907
|
+
this.ctx.rect(rect.x, rect.y, rect.width, rect.height);
|
|
908
|
+
});
|
|
909
|
+
this.ctx.fill();
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* ✅ OPTIMISATION OPTION 5: Utiliser ImageData pour les mises à jour fréquentes
|
|
915
|
+
* @param {number} x - Position X
|
|
916
|
+
* @param {number} y - Position Y
|
|
917
|
+
* @param {number} width - Largeur
|
|
918
|
+
* @param {number} height - Hauteur
|
|
919
|
+
* @param {Function} drawFn - Fonction pour manipuler les pixels
|
|
920
|
+
*/
|
|
921
|
+
updateRegion(x, y, width, height, drawFn) {
|
|
922
|
+
const imageData = this.ctx.getImageData(x, y, width, height);
|
|
923
|
+
const data = imageData.data;
|
|
924
|
+
|
|
925
|
+
// Manipuler directement les pixels
|
|
926
|
+
drawFn(data, width, height);
|
|
927
|
+
|
|
928
|
+
this.ctx.putImageData(imageData, x, y);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* ✅ OPTIMISATION OPTION 2: Flush du buffer pour le double buffering
|
|
933
|
+
*/
|
|
934
|
+
flush() {
|
|
935
|
+
if (this.optimizations.useDoubleBuffering && this._bufferCtx) {
|
|
936
|
+
// Copier tout le buffer sur le canvas réel
|
|
937
|
+
this.ctx.drawImage(this._doubleBuffer, 0, 0, this.width, this.height);
|
|
938
|
+
|
|
939
|
+
// Réinitialiser le buffer pour le prochain frame
|
|
940
|
+
this._bufferCtx.fillStyle = this.backgroundColor || '#ffffff';
|
|
941
|
+
this._bufferCtx.fillRect(0, 0, this.width, this.height);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* ✅ OPTIMISATION OPTION 5: Rendu optimisé avec viewport culling
|
|
947
|
+
* @private
|
|
948
|
+
*/
|
|
949
|
+
_renderOptimized() {
|
|
950
|
+
const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
|
|
951
|
+
|
|
952
|
+
if (!ctx) return;
|
|
953
|
+
|
|
954
|
+
// Clear le canvas
|
|
955
|
+
ctx.clearRect(0, 0, this.width, this.height);
|
|
956
|
+
|
|
957
|
+
// Séparer les composants fixes et scrollables
|
|
958
|
+
const scrollableComponents = [];
|
|
959
|
+
const fixedComponents = [];
|
|
960
|
+
|
|
961
|
+
for (let comp of this.components) {
|
|
962
|
+
if (this.isFixedComponent(comp)) {
|
|
963
|
+
fixedComponents.push(comp);
|
|
964
|
+
} else {
|
|
965
|
+
scrollableComponents.push(comp);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Rendu des composants scrollables avec viewport culling optimisé
|
|
970
|
+
ctx.save();
|
|
971
|
+
ctx.translate(0, this.scrollOffset);
|
|
972
|
+
|
|
973
|
+
// ✅ OPTIMISATION: Utiliser le spatial partitioning si activé
|
|
974
|
+
if (this.optimizations.useSpatialPartitioning && this._spatialGrid) {
|
|
975
|
+
const visibleComps = this._spatialGrid.getVisible(-this.scrollOffset);
|
|
976
|
+
for (let comp of visibleComps) {
|
|
977
|
+
if (comp.visible) {
|
|
978
|
+
comp.draw(ctx);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
} else {
|
|
982
|
+
// Rendu standard avec culling simple
|
|
983
|
+
for (let comp of scrollableComponents) {
|
|
984
|
+
if (comp.visible) {
|
|
985
|
+
const screenY = comp.y + this.scrollOffset;
|
|
986
|
+
const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
|
|
987
|
+
|
|
988
|
+
if (isInViewport) {
|
|
989
|
+
comp.draw(ctx);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
ctx.restore();
|
|
996
|
+
|
|
997
|
+
// Rendu des composants fixes
|
|
998
|
+
for (let comp of fixedComponents) {
|
|
999
|
+
if (comp.visible) {
|
|
1000
|
+
comp.draw(ctx);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Flush si on utilise le double buffering
|
|
1005
|
+
if (this.optimizations.useDoubleBuffering) {
|
|
1006
|
+
this.flush();
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* ✅ OPTIMISATION OPTION 2: Rendu partiel (seulement les composants sales)
|
|
1012
|
+
* @private
|
|
1013
|
+
*/
|
|
1014
|
+
_renderDirtyComponents() {
|
|
1015
|
+
const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
|
|
1016
|
+
|
|
1017
|
+
if (!ctx || this.dirtyComponents.size === 0) return;
|
|
1018
|
+
|
|
1019
|
+
// ✅ CORRECTION : Ne pas nettoyer avec fillRect !
|
|
1020
|
+
// À la place, utiliser le clipping pour redessiner proprement
|
|
1021
|
+
|
|
1022
|
+
// Copier dans un tableau
|
|
1023
|
+
const dirtyArray = Array.from(this.dirtyComponents);
|
|
1024
|
+
|
|
1025
|
+
// Vider immédiatement
|
|
1026
|
+
this.dirtyComponents.clear();
|
|
1027
|
+
|
|
1028
|
+
// Séparer les composants
|
|
1029
|
+
const fixedComps = [];
|
|
1030
|
+
const scrollableComps = [];
|
|
1031
|
+
|
|
1032
|
+
dirtyArray.forEach(comp => {
|
|
1033
|
+
if (!comp.visible) return;
|
|
1034
|
+
|
|
1035
|
+
if (this.isFixedComponent(comp)) {
|
|
1036
|
+
fixedComps.push(comp);
|
|
1037
|
+
} else {
|
|
1038
|
+
scrollableComps.push(comp);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// ✅ APPROCHE CORRECTE : Redessiner les composants sales
|
|
1043
|
+
// sans effacer leur zone d'abord
|
|
1044
|
+
|
|
1045
|
+
// 1. Dessiner les scrollables
|
|
1046
|
+
if (scrollableComps.length > 0) {
|
|
1047
|
+
ctx.save();
|
|
1048
|
+
ctx.translate(0, this.scrollOffset);
|
|
1049
|
+
|
|
1050
|
+
for (let comp of scrollableComps) {
|
|
1051
|
+
// ✅ NE PAS faire de fillRect !
|
|
1052
|
+
// Juste dessiner le composant
|
|
1053
|
+
if (comp.draw) {
|
|
1054
|
+
comp.draw(ctx);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (comp.markClean) {
|
|
1058
|
+
comp.markClean();
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
ctx.restore();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// 2. Dessiner les fixes
|
|
1066
|
+
for (let comp of fixedComps) {
|
|
1067
|
+
if (comp.draw) {
|
|
1068
|
+
comp.draw(ctx);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (comp.markClean) {
|
|
1072
|
+
comp.markClean();
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Flush si double buffering
|
|
1077
|
+
if (this.optimizations.useDoubleBuffering) {
|
|
1078
|
+
this.flush();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Active/désactive une optimisation spécifique
|
|
1084
|
+
* @param {string} optimization - Nom de l'optimisation
|
|
1085
|
+
* @param {boolean} enabled - true pour activer, false pour désactiver
|
|
1086
|
+
*/
|
|
1087
|
+
setOptimization(optimization, enabled) {
|
|
1088
|
+
if (this.optimizations.hasOwnProperty(optimization)) {
|
|
1089
|
+
this.optimizations[optimization] = enabled;
|
|
1090
|
+
|
|
1091
|
+
switch (optimization) {
|
|
1092
|
+
case 'useDoubleBuffering':
|
|
1093
|
+
if (enabled && !this._bufferCtx) {
|
|
1094
|
+
this._initDoubleBuffer();
|
|
1095
|
+
}
|
|
1096
|
+
break;
|
|
1097
|
+
case 'useSpatialPartitioning':
|
|
1098
|
+
if (enabled && !this._spatialGrid) {
|
|
1099
|
+
this._initSpatialPartitioning();
|
|
1100
|
+
}
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Marquer tous les composants comme sales pour forcer un redessin complet
|
|
1105
|
+
this.components.forEach(comp => this.markComponentDirty(comp));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Obtient l'état des optimisations
|
|
1111
|
+
* @returns {Object} État des optimisations
|
|
1112
|
+
*/
|
|
1113
|
+
getOptimizations() {
|
|
1114
|
+
return {
|
|
1115
|
+
...this.optimizations
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Affiche un écran de chargement animé
|
|
1121
|
+
* @private
|
|
1122
|
+
*/
|
|
1123
|
+
showSplashScreen() {
|
|
1124
|
+
const startTime = performance.now();
|
|
1125
|
+
const opts = this.splashOptions;
|
|
1126
|
+
|
|
1127
|
+
// ✅ Charger le logo si présent
|
|
1128
|
+
let logoImage = null;
|
|
1129
|
+
if (opts.logo) {
|
|
1130
|
+
logoImage = new Image();
|
|
1131
|
+
logoImage.src = opts.logo;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const animate = () => {
|
|
1135
|
+
const elapsed = performance.now() - startTime;
|
|
1136
|
+
const progress = Math.min(elapsed / opts.duration, 1);
|
|
1137
|
+
|
|
1138
|
+
// Clear
|
|
1139
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
1140
|
+
|
|
1141
|
+
// ✅ Background (gradient ou couleur unie)
|
|
1142
|
+
if (Array.isArray(opts.backgroundColor) && opts.backgroundColor.length >= 2) {
|
|
1143
|
+
// Gradient
|
|
1144
|
+
const gradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
|
|
1145
|
+
gradient.addColorStop(0, opts.backgroundColor[0]);
|
|
1146
|
+
gradient.addColorStop(1, opts.backgroundColor[1]);
|
|
1147
|
+
this.ctx.fillStyle = gradient;
|
|
1148
|
+
} else {
|
|
1149
|
+
// Couleur unie
|
|
1150
|
+
this.ctx.fillStyle = opts.backgroundColor;
|
|
1151
|
+
}
|
|
1152
|
+
this.ctx.fillRect(0, 0, this.width, this.height);
|
|
1153
|
+
|
|
1154
|
+
const centerX = this.width / 2;
|
|
1155
|
+
const centerY = this.height / 2;
|
|
1156
|
+
|
|
1157
|
+
// ✅ Logo (si présent et chargé)
|
|
1158
|
+
if (logoImage && logoImage.complete) {
|
|
1159
|
+
const logoX = centerX - opts.logoWidth / 2;
|
|
1160
|
+
const logoY = centerY - opts.logoHeight - 80;
|
|
1161
|
+
this.ctx.drawImage(logoImage, logoX, logoY, opts.logoWidth, opts.logoHeight);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// ✅ Spinner animé
|
|
1165
|
+
const radius = 40;
|
|
1166
|
+
const rotation = (elapsed / 1000) * Math.PI * 2;
|
|
1167
|
+
|
|
1168
|
+
// Cercle de fond
|
|
1169
|
+
this.ctx.beginPath();
|
|
1170
|
+
this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
1171
|
+
this.ctx.strokeStyle = opts.spinnerBackground;
|
|
1172
|
+
this.ctx.lineWidth = 4;
|
|
1173
|
+
this.ctx.stroke();
|
|
1174
|
+
|
|
1175
|
+
// Arc animé
|
|
1176
|
+
this.ctx.beginPath();
|
|
1177
|
+
this.ctx.arc(centerX, centerY, radius, rotation, rotation + Math.PI * 1.5);
|
|
1178
|
+
this.ctx.strokeStyle = opts.spinnerColor;
|
|
1179
|
+
this.ctx.lineWidth = 4;
|
|
1180
|
+
this.ctx.lineCap = 'round';
|
|
1181
|
+
this.ctx.stroke();
|
|
1182
|
+
|
|
1183
|
+
// ✅ Texte personnalisé
|
|
1184
|
+
this.ctx.fillStyle = opts.textColor;
|
|
1185
|
+
this.ctx.font = `${opts.textSize}px ${opts.textFont}`;
|
|
1186
|
+
this.ctx.textAlign = 'center';
|
|
1187
|
+
this.ctx.fillText(opts.text, centerX, centerY + radius + 40);
|
|
1188
|
+
|
|
1189
|
+
// ✅ Barre de progression (optionnelle)
|
|
1190
|
+
if (opts.showProgressBar) {
|
|
1191
|
+
const barWidth = 200;
|
|
1192
|
+
const barHeight = 4;
|
|
1193
|
+
const barX = centerX - barWidth / 2;
|
|
1194
|
+
const barY = centerY + radius + 70;
|
|
1195
|
+
|
|
1196
|
+
// Fond de la barre
|
|
1197
|
+
this.ctx.fillStyle = opts.progressBarBackground;
|
|
1198
|
+
this.ctx.fillRect(barX, barY, barWidth, barHeight);
|
|
1199
|
+
|
|
1200
|
+
// Progression
|
|
1201
|
+
this.ctx.fillStyle = opts.progressBarColor;
|
|
1202
|
+
this.ctx.fillRect(barX, barY, barWidth * progress, barHeight);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Continuer ou fade out
|
|
1206
|
+
if (progress < 1) {
|
|
1207
|
+
requestAnimationFrame(animate);
|
|
1208
|
+
} else {
|
|
1209
|
+
this.fadeOutSplash();
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
animate();
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Fade out du splash screen
|
|
1218
|
+
* @private
|
|
1219
|
+
*/
|
|
1220
|
+
fadeOutSplash() {
|
|
1221
|
+
const opts = this.splashOptions;
|
|
1222
|
+
const duration = opts.fadeOutDuration;
|
|
1223
|
+
const startTime = performance.now();
|
|
1224
|
+
|
|
1225
|
+
const fade = () => {
|
|
1226
|
+
const elapsed = performance.now() - startTime;
|
|
1227
|
+
const progress = elapsed / duration;
|
|
1228
|
+
const alpha = 1 - Math.min(progress, 1);
|
|
1229
|
+
|
|
1230
|
+
if (alpha > 0) {
|
|
1231
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
1232
|
+
this.ctx.globalAlpha = alpha;
|
|
1233
|
+
|
|
1234
|
+
// Redessiner le background
|
|
1235
|
+
if (Array.isArray(opts.backgroundColor) && opts.backgroundColor.length >= 2) {
|
|
1236
|
+
const gradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
|
|
1237
|
+
gradient.addColorStop(0, opts.backgroundColor[0]);
|
|
1238
|
+
gradient.addColorStop(1, opts.backgroundColor[1]);
|
|
1239
|
+
this.ctx.fillStyle = gradient;
|
|
1240
|
+
} else {
|
|
1241
|
+
this.ctx.fillStyle = opts.backgroundColor;
|
|
1242
|
+
}
|
|
1243
|
+
this.ctx.fillRect(0, 0, this.width, this.height);
|
|
1244
|
+
|
|
1245
|
+
// Spinner pendant le fade
|
|
1246
|
+
const centerX = this.width / 2;
|
|
1247
|
+
const centerY = this.height / 2;
|
|
1248
|
+
const radius = 40;
|
|
1249
|
+
|
|
1250
|
+
this.ctx.beginPath();
|
|
1251
|
+
this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
1252
|
+
this.ctx.strokeStyle = opts.spinnerBackground;
|
|
1253
|
+
this.ctx.lineWidth = 4;
|
|
1254
|
+
this.ctx.stroke();
|
|
1255
|
+
|
|
1256
|
+
this.ctx.globalAlpha = 1;
|
|
1257
|
+
requestAnimationFrame(fade);
|
|
1258
|
+
} else {
|
|
1259
|
+
this._splashFinished = true;
|
|
1260
|
+
// ✅ AJOUTER : Réinitialiser complètement le contexte
|
|
1261
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
1262
|
+
this.ctx.globalAlpha = 1;
|
|
1263
|
+
this.ctx.textAlign = 'start'; // ← IMPORTANT
|
|
1264
|
+
this.ctx.textBaseline = 'alphabetic'; // ← IMPORTANT
|
|
1265
|
+
this.ctx.font = '10px sans-serif'; // Valeur par défaut
|
|
1266
|
+
this.ctx.fillStyle = '#000000';
|
|
1267
|
+
this.ctx.strokeStyle = '#000000';
|
|
1268
|
+
this.ctx.lineWidth = 1;
|
|
1269
|
+
this.ctx.lineCap = 'butt';
|
|
1270
|
+
this.ctx.lineJoin = 'miter';
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
fade();
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// ✅ AJOUTER: Méthode pour mesurer le premier rendu
|
|
1278
|
+
_markFirstRender() {
|
|
1279
|
+
if (!this._firstRenderDone) {
|
|
1280
|
+
this._firstRenderDone = true;
|
|
1281
|
+
const firstRenderTime = performance.now() - this._startupStartTime - this.metrics.initTime;
|
|
1282
|
+
this.metrics.firstRenderTime = firstRenderTime;
|
|
1283
|
+
this.metrics.totalStartupTime = performance.now() - this._startupStartTime;
|
|
1284
|
+
|
|
1285
|
+
if (this.showMetrics) {
|
|
1286
|
+
console.log(`🎨 Premier rendu en ${firstRenderTime.toFixed(2)}ms`);
|
|
1287
|
+
console.log(`🚀 Temps total de démarrage: ${this.metrics.totalStartupTime.toFixed(2)}ms`);
|
|
1288
|
+
this.displayMetrics();
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Écoute les changements système (ex: utilisateur bascule dark mode)
|
|
1295
|
+
*/
|
|
1296
|
+
setupSystemThemeListener() {
|
|
1297
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
1298
|
+
|
|
1299
|
+
// Ancienne méthode (compatibilité large)
|
|
1300
|
+
if (mediaQuery.addEventListener) {
|
|
1301
|
+
mediaQuery.addEventListener('change', (e) => {
|
|
1302
|
+
if (this.themeMode === 'system') {
|
|
1303
|
+
this.applyThemeFromSystem();
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
} else {
|
|
1307
|
+
// Anciens navigateurs (rare en 2026)
|
|
1308
|
+
mediaQuery.addListener((e) => {
|
|
1309
|
+
if (this.themeMode === 'system') {
|
|
1310
|
+
this.applyThemeFromSystem();
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Change le mode thème
|
|
1318
|
+
* @param {'light'|'dark'|'system'} mode - Mode à appliquer
|
|
1319
|
+
* @param {boolean} [save=true] - Sauvegarder le choix utilisateur ?
|
|
1320
|
+
*/
|
|
1321
|
+
setThemeMode(mode) {
|
|
1322
|
+
this.themeManager.setMode(mode);
|
|
1323
|
+
this.theme = this.themeManager.getTheme();
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Obtient une couleur du thème
|
|
1328
|
+
*/
|
|
1329
|
+
getColor(colorName) {
|
|
1330
|
+
return this.themeManager.getColor(colorName);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Ajoute un listener de changement de thème
|
|
1335
|
+
*/
|
|
1336
|
+
onThemeChange(callback) {
|
|
1337
|
+
this.themeManager.addListener((theme) => {
|
|
1338
|
+
this.theme = theme;
|
|
1339
|
+
callback(theme);
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Bascule entre light et dark
|
|
1345
|
+
*/
|
|
1346
|
+
toggleTheme() {
|
|
1347
|
+
this.themeManager.toggle();
|
|
1348
|
+
this.theme = this.themeManager.getTheme();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Active ou désactive les DevTools
|
|
1353
|
+
* @param {boolean} enabled - true pour activer, false pour désactiver
|
|
1354
|
+
*/
|
|
1355
|
+
enableDevTools(enabled = true) {
|
|
1356
|
+
if (enabled) {
|
|
1357
|
+
// Créer le DevTools s'il n'existe pas
|
|
1358
|
+
if (!this.devTools) {
|
|
1359
|
+
this.devTools = new DevTools(this);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Attacher seulement si pas déjà fait
|
|
1363
|
+
if (!this.devTools._isAttached) {
|
|
1364
|
+
this.devTools.attachToFramework();
|
|
1365
|
+
this.devTools._isAttached = true;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Afficher le bouton
|
|
1369
|
+
if (this.devTools.toggleBtn) {
|
|
1370
|
+
this.devTools.toggleBtn.style.display = 'block';
|
|
1371
|
+
}
|
|
1372
|
+
} else {
|
|
1373
|
+
// Désactiver complètement
|
|
1374
|
+
if (this.devTools) {
|
|
1375
|
+
// Détacher du framework
|
|
1376
|
+
if (this.devTools.detachFromFramework) {
|
|
1377
|
+
this.devTools.detachFromFramework();
|
|
1378
|
+
} else if (this.devTools.cleanup) {
|
|
1379
|
+
this.devTools.cleanup();
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Supprimer de la page DOM
|
|
1383
|
+
if (this.devTools.container && this.devTools.container.parentNode) {
|
|
1384
|
+
this.devTools.container.parentNode.removeChild(this.devTools.container);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
if (this.devTools.toggleBtn && this.devTools.toggleBtn.parentNode) {
|
|
1388
|
+
this.devTools.toggleBtn.parentNode.removeChild(this.devTools.toggleBtn);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
this.devTools._isAttached = false;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Bascule l'overlay d'inspection
|
|
1398
|
+
*/
|
|
1399
|
+
toggleInspection() {
|
|
1400
|
+
this.inspectionOverlay.toggle();
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Exécute une commande DevTools
|
|
1405
|
+
*/
|
|
1406
|
+
devToolsCommand(command, ...args) {
|
|
1407
|
+
switch (command) {
|
|
1408
|
+
case 'inspect':
|
|
1409
|
+
this.inspectionOverlay.enable();
|
|
1410
|
+
break;
|
|
1411
|
+
case 'performance':
|
|
1412
|
+
this.devTools.switchTab('performance');
|
|
1413
|
+
this.devTools.toggle();
|
|
1414
|
+
break;
|
|
1415
|
+
case 'components':
|
|
1416
|
+
this.devTools.switchTab('components');
|
|
1417
|
+
this.devTools.toggle();
|
|
1418
|
+
break;
|
|
1419
|
+
case 'highlight':
|
|
1420
|
+
if (args[0]) {
|
|
1421
|
+
this.devTools.highlightComponent(args[0]);
|
|
1422
|
+
}
|
|
1423
|
+
break;
|
|
1424
|
+
case 'reflow':
|
|
1425
|
+
this.components.forEach(comp => comp.markDirty());
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
wrapContext(ctx, theme) {
|
|
1431
|
+
const originalFillStyle = Object.getOwnPropertyDescriptor(CanvasRenderingContext2D.prototype, 'fillStyle');
|
|
1432
|
+
Object.defineProperty(ctx, 'fillStyle', {
|
|
1433
|
+
set: (value) => {
|
|
1434
|
+
// Si value est blanc/noir ou une couleur “neutre”, tu remplaces par theme
|
|
1435
|
+
if (value === '#FFFFFF' || value === '#000000') {
|
|
1436
|
+
originalFillStyle.set.call(ctx, value);
|
|
1437
|
+
} else {
|
|
1438
|
+
originalFillStyle.set.call(ctx, value);
|
|
1439
|
+
}
|
|
1440
|
+
},
|
|
1441
|
+
get: () => originalFillStyle.get.call(ctx)
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
createCanvasWorker() {
|
|
1446
|
+
const workerCode = `
|
|
1447
|
+
let components = [];
|
|
1448
|
+
|
|
1449
|
+
self.onmessage = function(e) {
|
|
1450
|
+
const { type, payload } = e.data;
|
|
1451
|
+
|
|
1452
|
+
switch(type) {
|
|
1453
|
+
case 'INIT':
|
|
1454
|
+
components = payload.components;
|
|
1455
|
+
self.postMessage({ type: 'READY' });
|
|
1456
|
+
break;
|
|
1457
|
+
|
|
1458
|
+
case 'UPDATE_LAYOUT':
|
|
1459
|
+
const updated = components.map(comp => {
|
|
1460
|
+
if (comp.dynamicHeight && comp.calculateHeight) {
|
|
1461
|
+
comp.height = comp.calculateHeight();
|
|
1462
|
+
}
|
|
1463
|
+
return { id: comp.id, height: comp.height };
|
|
1464
|
+
});
|
|
1465
|
+
self.postMessage({ type: 'LAYOUT_DONE', payload: updated });
|
|
1466
|
+
break;
|
|
1467
|
+
|
|
1468
|
+
case 'SCROLL_INERTIA':
|
|
1469
|
+
let { offset, velocity, friction, maxScroll } = payload;
|
|
1470
|
+
offset += velocity;
|
|
1471
|
+
offset = Math.max(Math.min(offset, 0), -maxScroll);
|
|
1472
|
+
velocity *= friction;
|
|
1473
|
+
self.postMessage({ type: 'SCROLL_UPDATED', payload: { offset, velocity } });
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
`;
|
|
1478
|
+
|
|
1479
|
+
const blob = new Blob([workerCode], {
|
|
1480
|
+
type: 'application/javascript'
|
|
1481
|
+
});
|
|
1482
|
+
return new Worker(URL.createObjectURL(blob));
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
createLogicWorker() {
|
|
1486
|
+
const workerCode = `
|
|
1487
|
+
let state = {};
|
|
1488
|
+
|
|
1489
|
+
self.onmessage = async function(e) {
|
|
1490
|
+
const { type, payload } = e.data;
|
|
1491
|
+
|
|
1492
|
+
switch(type) {
|
|
1493
|
+
case 'SET_STATE':
|
|
1494
|
+
state = payload;
|
|
1495
|
+
self.postMessage({ type: 'STATE_UPDATED', payload: state });
|
|
1496
|
+
break;
|
|
1497
|
+
|
|
1498
|
+
case 'EXECUTE':
|
|
1499
|
+
try {
|
|
1500
|
+
const fn = new Function('state', 'args', payload.fnString);
|
|
1501
|
+
const result = await fn(state, payload.args);
|
|
1502
|
+
self.postMessage({ type: 'EXECUTION_RESULT', payload: result });
|
|
1503
|
+
} catch (err) {
|
|
1504
|
+
self.postMessage({ type: 'EXECUTION_ERROR', payload: err.message });
|
|
1505
|
+
}
|
|
1506
|
+
break;
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
`;
|
|
1510
|
+
|
|
1511
|
+
const blob = new Blob([workerCode], {
|
|
1512
|
+
type: 'application/javascript'
|
|
1513
|
+
});
|
|
1514
|
+
return new Worker(URL.createObjectURL(blob));
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Set Theme dynamique
|
|
1518
|
+
setTheme(theme) {
|
|
1519
|
+
this.theme = theme;
|
|
1520
|
+
|
|
1521
|
+
if (!this.useWebGL) {
|
|
1522
|
+
this.wrapContext(this.ctx, theme);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Protège la boucle
|
|
1526
|
+
if (this.components && Array.isArray(this.components)) {
|
|
1527
|
+
this.components.forEach(comp => comp.markDirty());
|
|
1528
|
+
} else {
|
|
1529
|
+
console.warn('[setTheme] components pas encore initialisé');
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// Switch Theme
|
|
1534
|
+
toggleDarkMode() {
|
|
1535
|
+
if (this.theme === lightTheme) {
|
|
1536
|
+
this.setTheme(darkTheme);
|
|
1537
|
+
} else {
|
|
1538
|
+
this.setTheme(lightTheme);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
enableFpsDisplay(enable = true) {
|
|
1543
|
+
this.showFps = enable;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// AJOUTER CETTE MÉTHODE (optionnel - pour faciliter l'accès)
|
|
1547
|
+
animate(component, options) {
|
|
1548
|
+
return this.animator.animate(component, options);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// ----- Worker UI -----
|
|
1552
|
+
handleWorkerMessage(e) {
|
|
1553
|
+
const {
|
|
1554
|
+
type,
|
|
1555
|
+
payload
|
|
1556
|
+
} = e.data;
|
|
1557
|
+
switch (type) {
|
|
1558
|
+
case 'LAYOUT_DONE':
|
|
1559
|
+
for (let update of payload) {
|
|
1560
|
+
const comp = this.components.find(c => c.id === update.id);
|
|
1561
|
+
if (comp) comp.height = update.height;
|
|
1562
|
+
}
|
|
1563
|
+
break;
|
|
1564
|
+
case 'SCROLL_UPDATED':
|
|
1565
|
+
this.scrollOffset = payload.offset;
|
|
1566
|
+
this.scrollVelocity = payload.velocity;
|
|
1567
|
+
break;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
updateLayoutAsync() {
|
|
1572
|
+
this.worker.postMessage({
|
|
1573
|
+
type: 'UPDATE_LAYOUT'
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
updateScrollInertia() {
|
|
1578
|
+
const maxScroll = this.getMaxScroll();
|
|
1579
|
+
this.worker.postMessage({
|
|
1580
|
+
type: 'SCROLL_INERTIA',
|
|
1581
|
+
payload: {
|
|
1582
|
+
offset: this.scrollOffset,
|
|
1583
|
+
velocity: this.scrollVelocity,
|
|
1584
|
+
friction: this.scrollFriction,
|
|
1585
|
+
maxScroll
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// ------ Logic Worker --------
|
|
1591
|
+
handleLogicWorkerMessage(e) {
|
|
1592
|
+
const {
|
|
1593
|
+
type,
|
|
1594
|
+
payload
|
|
1595
|
+
} = e.data;
|
|
1596
|
+
switch (type) {
|
|
1597
|
+
case 'STATE_UPDATED':
|
|
1598
|
+
// Le worker a renvoyé le nouvel état global
|
|
1599
|
+
this.logicWorkerState = payload;
|
|
1600
|
+
break;
|
|
1601
|
+
|
|
1602
|
+
case 'EXECUTION_RESULT':
|
|
1603
|
+
// Résultat d'une tâche spécifique envoyée au worker
|
|
1604
|
+
if (this.onWorkerResult) this.onWorkerResult(payload);
|
|
1605
|
+
break;
|
|
1606
|
+
|
|
1607
|
+
case 'EXECUTION_ERROR':
|
|
1608
|
+
console.error('Logic Worker Error:', payload);
|
|
1609
|
+
break;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
runLogicTask(taskName, taskData) {
|
|
1614
|
+
this.logicWorker.postMessage({
|
|
1615
|
+
type: 'EXECUTE_TASK',
|
|
1616
|
+
payload: {
|
|
1617
|
+
taskName,
|
|
1618
|
+
taskData
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
updateLogicWorkerState(newState) {
|
|
1624
|
+
this.logicWorkerState = {
|
|
1625
|
+
...this.logicWorkerState,
|
|
1626
|
+
...newState
|
|
1627
|
+
};
|
|
1628
|
+
this.logicWorker.postMessage({
|
|
1629
|
+
type: 'SET_STATE',
|
|
1630
|
+
payload: this.logicWorkerState
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
detectPlatform() {
|
|
1635
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
1636
|
+
if (/android/.test(ua)) return 'material';
|
|
1637
|
+
if (/iphone|ipad|ipod/.test(ua)) return 'cupertino';
|
|
1638
|
+
return 'material';
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
setupCanvas() {
|
|
1642
|
+
this.canvas.width = this.width * this.dpr;
|
|
1643
|
+
this.canvas.height = this.height * this.dpr;
|
|
1644
|
+
this.canvas.style.width = this.width + 'px';
|
|
1645
|
+
this.canvas.style.height = this.height + 'px';
|
|
1646
|
+
|
|
1647
|
+
// ✅ AJOUTER: Appliquer le background au style CSS
|
|
1648
|
+
this.canvas.style.backgroundColor = this.backgroundColor;
|
|
1649
|
+
// Échelle uniquement pour Canvas 2D
|
|
1650
|
+
this.ctx.scale(this.dpr, this.dpr);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
setupEventListeners() {
|
|
1654
|
+
this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
|
|
1655
|
+
this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
|
|
1656
|
+
this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
|
|
1657
|
+
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
|
1658
|
+
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
|
1659
|
+
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
|
1660
|
+
window.addEventListener('resize', this.handleResize.bind(this));
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* Configure l'écoute de l'historique du navigateur
|
|
1665
|
+
* @private
|
|
1666
|
+
*/
|
|
1667
|
+
setupHistoryListener() {
|
|
1668
|
+
window.addEventListener('popstate', (e) => {
|
|
1669
|
+
if (e.state && e.state.route) {
|
|
1670
|
+
this.navigateTo(e.state.route, {
|
|
1671
|
+
replace: true,
|
|
1672
|
+
animate: true,
|
|
1673
|
+
direction: 'back'
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// ===== MÉTHODES DE ROUTING =====
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* Définit une route avec pattern de paramètres
|
|
1683
|
+
* @param {string} pattern - Pattern de la route (ex: '/user/:id', '/posts/:category/:id')
|
|
1684
|
+
* @param {Function} component - Fonction qui crée les composants
|
|
1685
|
+
* @param {Object} options - Options de la route
|
|
1686
|
+
* @returns {CanvasFramework}
|
|
1687
|
+
*/
|
|
1688
|
+
route(pattern, component, options = {}) {
|
|
1689
|
+
const route = {
|
|
1690
|
+
pattern,
|
|
1691
|
+
component,
|
|
1692
|
+
regex: this.patternToRegex(pattern),
|
|
1693
|
+
paramNames: this.extractParamNames(pattern),
|
|
1694
|
+
beforeEnter: options.beforeEnter,
|
|
1695
|
+
afterEnter: options.afterEnter,
|
|
1696
|
+
beforeLeave: options.beforeLeave,
|
|
1697
|
+
afterLeave: options.afterLeave, // ✅ NOUVEAU
|
|
1698
|
+
onEnter: options.onEnter, // ✅ NOUVEAU (alias de afterEnter)
|
|
1699
|
+
onLeave: options.onLeave, // ✅ NOUVEAU (alias de beforeLeave)
|
|
1700
|
+
transition: options.transition || 'slide'
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
this.routes.set(pattern, route);
|
|
1704
|
+
return this;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* Convertit un pattern de route en regex
|
|
1709
|
+
* @private
|
|
1710
|
+
*/
|
|
1711
|
+
patternToRegex(pattern) {
|
|
1712
|
+
const regexPattern = pattern
|
|
1713
|
+
.replace(/\//g, '\\/')
|
|
1714
|
+
.replace(/:([^\/]+)/g, '([^\\/]+)');
|
|
1715
|
+
return new RegExp(`^${regexPattern}$`);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Extrait les noms des paramètres d'un pattern
|
|
1720
|
+
* @private
|
|
1721
|
+
*/
|
|
1722
|
+
extractParamNames(pattern) {
|
|
1723
|
+
const matches = pattern.match(/:([^\/]+)/g);
|
|
1724
|
+
return matches ? matches.map(m => m.slice(1)) : [];
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Trouve la route correspondant à un path
|
|
1729
|
+
* @private
|
|
1730
|
+
*/
|
|
1731
|
+
matchRoute(path) {
|
|
1732
|
+
// Séparer le path et la query string
|
|
1733
|
+
const [pathname, queryString] = path.split('?');
|
|
1734
|
+
|
|
1735
|
+
for (let [pattern, route] of this.routes) {
|
|
1736
|
+
const match = pathname.match(route.regex);
|
|
1737
|
+
if (match) {
|
|
1738
|
+
const params = {};
|
|
1739
|
+
route.paramNames.forEach((name, index) => {
|
|
1740
|
+
params[name] = match[index + 1];
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
const query = this.parseQueryString(queryString);
|
|
1744
|
+
|
|
1745
|
+
return {
|
|
1746
|
+
route,
|
|
1747
|
+
params,
|
|
1748
|
+
query,
|
|
1749
|
+
pathname
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return null;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/**
|
|
1757
|
+
* Parse une query string
|
|
1758
|
+
* @private
|
|
1759
|
+
*/
|
|
1760
|
+
parseQueryString(queryString) {
|
|
1761
|
+
if (!queryString) return {};
|
|
1762
|
+
|
|
1763
|
+
const params = {};
|
|
1764
|
+
queryString.split('&').forEach(param => {
|
|
1765
|
+
const [key, value] = param.split('=');
|
|
1766
|
+
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
|
1767
|
+
});
|
|
1768
|
+
return params;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* Navigue vers une route
|
|
1773
|
+
* @param {string} path - Chemin de destination (ex: '/user/123', '/posts/tech/456?sort=date')
|
|
1774
|
+
* @param {Object} options - Options de navigation
|
|
1775
|
+
*/
|
|
1776
|
+
navigate(path, options = {}) {
|
|
1777
|
+
this.navigateTo(path, options);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/**
|
|
1781
|
+
* Méthode interne de navigation
|
|
1782
|
+
* @private
|
|
1783
|
+
*/
|
|
1784
|
+
async navigateTo(path, options = {}) {
|
|
1785
|
+
const {
|
|
1786
|
+
replace = false,
|
|
1787
|
+
animate = true,
|
|
1788
|
+
direction = 'forward',
|
|
1789
|
+
transition = null,
|
|
1790
|
+
state = {}
|
|
1791
|
+
} = options;
|
|
1792
|
+
|
|
1793
|
+
const match = this.matchRoute(path);
|
|
1794
|
+
if (!match) {
|
|
1795
|
+
console.warn(`Route not found: ${path}`);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const { route, params, query, pathname } = match;
|
|
1800
|
+
|
|
1801
|
+
// ===== LIFECYCLE: AVANT DE QUITTER L'ANCIENNE ROUTE =====
|
|
1802
|
+
const currentRouteData = this.routes.get(this.currentRoute);
|
|
1803
|
+
if (currentRouteData?.beforeLeave) {
|
|
1804
|
+
const canLeave = await currentRouteData.beforeLeave(this.currentParams, this.currentQuery, this);
|
|
1805
|
+
if (canLeave === false) {
|
|
1806
|
+
console.log('Navigation cancelled by beforeLeave hook');
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (currentRouteData?.onLeave) {
|
|
1812
|
+
await currentRouteData.onLeave(this.currentParams, this.currentQuery, this);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// ===== LIFECYCLE: AVANT D'ENTRER DANS LA NOUVELLE ROUTE =====
|
|
1816
|
+
if (route.beforeEnter) {
|
|
1817
|
+
const canEnter = await route.beforeEnter(params, query, this);
|
|
1818
|
+
if (canEnter === false) {
|
|
1819
|
+
console.log('Navigation cancelled by beforeEnter hook');
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
if (route.onEnter) {
|
|
1825
|
+
await route.onEnter(params, query, this);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// ===== SAUVEGARDER L'ÉTAT ACTUEL =====
|
|
1829
|
+
const oldComponents = [...this.components];
|
|
1830
|
+
const oldRoute = this.currentRoute;
|
|
1831
|
+
const oldParams = { ...this.currentParams };
|
|
1832
|
+
const oldQuery = { ...this.currentQuery };
|
|
1833
|
+
|
|
1834
|
+
// ✅ CRUCIAL : ARRÊTER LES ANCIENNES CAMÉRAS AVANT DE CRÉER LES NOUVELLES
|
|
1835
|
+
oldComponents.forEach(comp => {
|
|
1836
|
+
if (comp.constructor.name === 'FloatedCamera' ||
|
|
1837
|
+
comp.constructor.name === 'Camera' ||
|
|
1838
|
+
comp.constructor.name === 'Video' ||
|
|
1839
|
+
comp.constructor.name === 'QRCodeReader') {
|
|
1840
|
+
if (comp.stopCamera && typeof comp.stopCamera === 'function') {
|
|
1841
|
+
comp.stopCamera();
|
|
1842
|
+
}
|
|
1843
|
+
// Destroy le composant pour être sûr
|
|
1844
|
+
if (comp.destroy && typeof comp.destroy === 'function') {
|
|
1845
|
+
comp.destroy();
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
// ✅ Nettoyer toutes les vidéos orphelines AVANT de créer les nouveaux composants
|
|
1851
|
+
const allVideos = document.querySelectorAll('video');
|
|
1852
|
+
allVideos.forEach(v => v.remove());
|
|
1853
|
+
|
|
1854
|
+
// ===== METTRE À JOUR L'ÉTAT =====
|
|
1855
|
+
this.currentRoute = pathname;
|
|
1856
|
+
this.currentParams = params;
|
|
1857
|
+
this.currentQuery = query;
|
|
1858
|
+
|
|
1859
|
+
// ===== GÉRER L'HISTORIQUE =====
|
|
1860
|
+
if (!replace) {
|
|
1861
|
+
this.historyIndex++;
|
|
1862
|
+
this.history = this.history.slice(0, this.historyIndex);
|
|
1863
|
+
this.history.push({ path, params, query, state });
|
|
1864
|
+
window.history.pushState({ route: path, params, query, state }, '', path);
|
|
1865
|
+
} else {
|
|
1866
|
+
this.history[this.historyIndex] = { path, params, query, state };
|
|
1867
|
+
window.history.replaceState({ route: path, params, query, state }, '', path);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// ===== CRÉER LES NOUVEAUX COMPOSANTS =====
|
|
1871
|
+
this.components = [];
|
|
1872
|
+
if (typeof route.component === 'function') {
|
|
1873
|
+
route.component(this, params, query);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// ===== LANCER L'ANIMATION DE TRANSITION =====
|
|
1877
|
+
if (animate && !this.transitionState.isTransitioning) {
|
|
1878
|
+
const transitionType = transition || route.transition || 'slide';
|
|
1879
|
+
this.startTransition(oldComponents, this.components, transitionType, direction);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// ===== LIFECYCLE: APRÈS ÊTRE ENTRÉ DANS LA NOUVELLE ROUTE =====
|
|
1883
|
+
if (route.afterEnter) {
|
|
1884
|
+
route.afterEnter(params, query, this);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
if (currentRouteData?.afterLeave) {
|
|
1888
|
+
if (animate && this.transitionState.isTransitioning) {
|
|
1889
|
+
setTimeout(() => {
|
|
1890
|
+
currentRouteData.afterLeave(oldParams, oldQuery, this);
|
|
1891
|
+
}, this.transitionState.duration || 300);
|
|
1892
|
+
} else {
|
|
1893
|
+
currentRouteData.afterLeave(oldParams, oldQuery, this);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
this._maxScrollDirty = true;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
/**
|
|
1901
|
+
* Démarre une animation de transition
|
|
1902
|
+
* @private
|
|
1903
|
+
*/
|
|
1904
|
+
startTransition(oldComponents, newComponents, type, direction) {
|
|
1905
|
+
this.transitionState = {
|
|
1906
|
+
isTransitioning: true,
|
|
1907
|
+
progress: 0,
|
|
1908
|
+
duration: 300,
|
|
1909
|
+
type,
|
|
1910
|
+
direction,
|
|
1911
|
+
oldComponents: [...oldComponents],
|
|
1912
|
+
newComponents: [...newComponents],
|
|
1913
|
+
startTime: Date.now()
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* Met à jour l'animation de transition
|
|
1919
|
+
* @private
|
|
1920
|
+
*/
|
|
1921
|
+
updateTransition() {
|
|
1922
|
+
if (!this.transitionState.isTransitioning) return;
|
|
1923
|
+
|
|
1924
|
+
const elapsed = Date.now() - this.transitionState.startTime;
|
|
1925
|
+
this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
|
|
1926
|
+
|
|
1927
|
+
// Fonction d'easing (ease-in-out)
|
|
1928
|
+
const eased = this.easeInOutCubic(this.transitionState.progress);
|
|
1929
|
+
|
|
1930
|
+
// Appliquer la transformation selon le type
|
|
1931
|
+
this.ctx.save();
|
|
1932
|
+
this.applyTransitionTransform(eased);
|
|
1933
|
+
this.ctx.restore();
|
|
1934
|
+
|
|
1935
|
+
// Terminer la transition
|
|
1936
|
+
if (this.transitionState.progress >= 1) {
|
|
1937
|
+
this.transitionState.isTransitioning = false;
|
|
1938
|
+
this.transitionState.oldComponents = [];
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/**
|
|
1943
|
+
* Applique la transformation de transition
|
|
1944
|
+
* @private
|
|
1945
|
+
*/
|
|
1946
|
+
applyTransitionTransform(progress) {
|
|
1947
|
+
const {
|
|
1948
|
+
type,
|
|
1949
|
+
direction,
|
|
1950
|
+
oldComponents,
|
|
1951
|
+
newComponents
|
|
1952
|
+
} = this.transitionState;
|
|
1953
|
+
const directionMultiplier = direction === 'forward' ? 1 : -1;
|
|
1954
|
+
|
|
1955
|
+
switch (type) {
|
|
1956
|
+
case 'slide':
|
|
1957
|
+
// Dessiner l'ancienne vue qui sort
|
|
1958
|
+
this.ctx.save();
|
|
1959
|
+
this.ctx.translate(-this.width * progress * directionMultiplier, 0);
|
|
1960
|
+
this.ctx.globalAlpha = 1 - progress * 0.3;
|
|
1961
|
+
for (let comp of oldComponents) {
|
|
1962
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
1963
|
+
}
|
|
1964
|
+
this.ctx.restore();
|
|
1965
|
+
|
|
1966
|
+
// Dessiner la nouvelle vue qui entre
|
|
1967
|
+
this.ctx.save();
|
|
1968
|
+
this.ctx.translate(this.width * (1 - progress) * directionMultiplier, 0);
|
|
1969
|
+
for (let comp of newComponents) {
|
|
1970
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
1971
|
+
}
|
|
1972
|
+
this.ctx.restore();
|
|
1973
|
+
break;
|
|
1974
|
+
|
|
1975
|
+
case 'fade':
|
|
1976
|
+
// Dessiner l'ancienne vue qui fade out
|
|
1977
|
+
this.ctx.save();
|
|
1978
|
+
this.ctx.globalAlpha = 1 - progress;
|
|
1979
|
+
for (let comp of oldComponents) {
|
|
1980
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
1981
|
+
}
|
|
1982
|
+
this.ctx.restore();
|
|
1983
|
+
|
|
1984
|
+
// Dessiner la nouvelle vue qui fade in
|
|
1985
|
+
this.ctx.save();
|
|
1986
|
+
this.ctx.globalAlpha = progress;
|
|
1987
|
+
for (let comp of newComponents) {
|
|
1988
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
1989
|
+
}
|
|
1990
|
+
this.ctx.restore();
|
|
1991
|
+
break;
|
|
1992
|
+
|
|
1993
|
+
case 'none':
|
|
1994
|
+
// Pas d'animation, juste afficher la nouvelle vue
|
|
1995
|
+
for (let comp of newComponents) {
|
|
1996
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
1997
|
+
}
|
|
1998
|
+
break;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
/**
|
|
2003
|
+
* Fonction d'easing
|
|
2004
|
+
* @private
|
|
2005
|
+
*/
|
|
2006
|
+
easeInOutCubic(t) {
|
|
2007
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
/**
|
|
2011
|
+
* Retour en arrière dans l'historique
|
|
2012
|
+
*/
|
|
2013
|
+
goBack() {
|
|
2014
|
+
if (this.historyIndex > 0) {
|
|
2015
|
+
this.historyIndex--;
|
|
2016
|
+
const historyEntry = this.history[this.historyIndex];
|
|
2017
|
+
this.navigateTo(historyEntry.path, {
|
|
2018
|
+
replace: true,
|
|
2019
|
+
animate: true,
|
|
2020
|
+
direction: 'back'
|
|
2021
|
+
});
|
|
2022
|
+
window.history.back();
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/**
|
|
2027
|
+
* Avancer dans l'historique
|
|
2028
|
+
*/
|
|
2029
|
+
goForward() {
|
|
2030
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
2031
|
+
this.historyIndex++;
|
|
2032
|
+
const historyEntry = this.history[this.historyIndex];
|
|
2033
|
+
this.navigateTo(historyEntry.path, {
|
|
2034
|
+
replace: true,
|
|
2035
|
+
animate: true,
|
|
2036
|
+
direction: 'forward'
|
|
2037
|
+
});
|
|
2038
|
+
window.history.forward();
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* Obtient les paramètres de la route actuelle
|
|
2044
|
+
* @returns {Object}
|
|
2045
|
+
*/
|
|
2046
|
+
getParams() {
|
|
2047
|
+
return {
|
|
2048
|
+
...this.currentParams
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/**
|
|
2053
|
+
* Obtient la query string de la route actuelle
|
|
2054
|
+
* @returns {Object}
|
|
2055
|
+
*/
|
|
2056
|
+
getQuery() {
|
|
2057
|
+
return {
|
|
2058
|
+
...this.currentQuery
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
/**
|
|
2063
|
+
* Obtient un paramètre spécifique
|
|
2064
|
+
* @param {string} name
|
|
2065
|
+
* @returns {string|undefined}
|
|
2066
|
+
*/
|
|
2067
|
+
getParam(name) {
|
|
2068
|
+
return this.currentParams[name];
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/**
|
|
2072
|
+
* Obtient un paramètre de query spécifique
|
|
2073
|
+
* @param {string} name
|
|
2074
|
+
* @returns {string|undefined}
|
|
2075
|
+
*/
|
|
2076
|
+
getQueryParam(name) {
|
|
2077
|
+
return this.currentQuery[name];
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// ===== FIN DES MÉTHODES DE ROUTING =====
|
|
2081
|
+
|
|
2082
|
+
handleTouchStart(e) {
|
|
2083
|
+
e.preventDefault();
|
|
2084
|
+
this.isDragging = false;
|
|
2085
|
+
const touch = e.touches[0];
|
|
2086
|
+
const pos = this.getTouchPos(touch);
|
|
2087
|
+
this.lastTouchY = pos.y;
|
|
2088
|
+
|
|
2089
|
+
// Informer le Worker
|
|
2090
|
+
this.scrollWorker.postMessage({
|
|
2091
|
+
type: 'SET_DRAGGING',
|
|
2092
|
+
payload: {
|
|
2093
|
+
isDragging: false,
|
|
2094
|
+
lastTouchY: this.lastTouchY
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'start');
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
handleTouchMove(e) {
|
|
2102
|
+
e.preventDefault();
|
|
2103
|
+
const touch = e.touches[0];
|
|
2104
|
+
const pos = this.getTouchPos(touch);
|
|
2105
|
+
|
|
2106
|
+
if (!this.isDragging) {
|
|
2107
|
+
const deltaY = Math.abs(pos.y - this.lastTouchY);
|
|
2108
|
+
if (deltaY > 5) {
|
|
2109
|
+
this.isDragging = true;
|
|
2110
|
+
this.scrollWorker.postMessage({
|
|
2111
|
+
type: 'SET_DRAGGING',
|
|
2112
|
+
payload: {
|
|
2113
|
+
isDragging: true,
|
|
2114
|
+
lastTouchY: this.lastTouchY
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
if (this.isDragging) {
|
|
2121
|
+
const deltaY = pos.y - this.lastTouchY;
|
|
2122
|
+
|
|
2123
|
+
// Déléguer le calcul au Worker
|
|
2124
|
+
this.scrollWorker.postMessage({
|
|
2125
|
+
type: 'HANDLE_TOUCH_MOVE',
|
|
2126
|
+
payload: {
|
|
2127
|
+
deltaY
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
this.lastTouchY = pos.y;
|
|
2132
|
+
} else {
|
|
2133
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'move');
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
handleTouchEnd(e) {
|
|
2138
|
+
e.preventDefault();
|
|
2139
|
+
const touch = e.changedTouches[0];
|
|
2140
|
+
const pos = this.getTouchPos(touch);
|
|
2141
|
+
|
|
2142
|
+
if (!this.isDragging) {
|
|
2143
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'end');
|
|
2144
|
+
} else {
|
|
2145
|
+
this.isDragging = false;
|
|
2146
|
+
this.scrollWorker.postMessage({
|
|
2147
|
+
type: 'SET_DRAGGING',
|
|
2148
|
+
payload: {
|
|
2149
|
+
isDragging: false,
|
|
2150
|
+
lastVelocity: this.scrollVelocity
|
|
2151
|
+
}
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
handleMouseDown(e) {
|
|
2157
|
+
this.isDragging = false;
|
|
2158
|
+
this.lastTouchY = e.clientY;
|
|
2159
|
+
|
|
2160
|
+
this.scrollWorker.postMessage({
|
|
2161
|
+
type: 'SET_DRAGGING',
|
|
2162
|
+
payload: {
|
|
2163
|
+
isDragging: false,
|
|
2164
|
+
lastTouchY: this.lastTouchY
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
|
|
2168
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'start');
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
handleMouseMove(e) {
|
|
2172
|
+
if (!this.isDragging) {
|
|
2173
|
+
const deltaY = Math.abs(e.clientY - this.lastTouchY);
|
|
2174
|
+
if (deltaY > 5) {
|
|
2175
|
+
this.isDragging = true;
|
|
2176
|
+
this.scrollWorker.postMessage({
|
|
2177
|
+
type: 'SET_DRAGGING',
|
|
2178
|
+
payload: {
|
|
2179
|
+
isDragging: true,
|
|
2180
|
+
lastTouchY: this.lastTouchY
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
if (this.isDragging) {
|
|
2187
|
+
const deltaY = e.clientY - this.lastTouchY;
|
|
2188
|
+
|
|
2189
|
+
this.scrollWorker.postMessage({
|
|
2190
|
+
type: 'HANDLE_TOUCH_MOVE',
|
|
2191
|
+
payload: {
|
|
2192
|
+
deltaY
|
|
2193
|
+
}
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
this.lastTouchY = e.clientY;
|
|
2197
|
+
} else {
|
|
2198
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'move');
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
handleMouseUp(e) {
|
|
2203
|
+
if (!this.isDragging) {
|
|
2204
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
|
|
2205
|
+
} else {
|
|
2206
|
+
this.isDragging = false;
|
|
2207
|
+
this.scrollWorker.postMessage({
|
|
2208
|
+
type: 'SET_DRAGGING',
|
|
2209
|
+
payload: {
|
|
2210
|
+
isDragging: false,
|
|
2211
|
+
lastVelocity: this.scrollVelocity
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
|
|
2218
|
+
getTouchPos(touch) {
|
|
2219
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
2220
|
+
return {
|
|
2221
|
+
x: touch.clientX - rect.left,
|
|
2222
|
+
y: touch.clientY - rect.top
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
checkComponentsAtPosition(x, y, eventType) {
|
|
2227
|
+
const isFixedComponent = (comp) =>
|
|
2228
|
+
FIXED_COMPONENT_TYPES.has(comp.constructor);
|
|
2229
|
+
|
|
2230
|
+
for (let i = this.components.length - 1; i >= 0; i--) {
|
|
2231
|
+
const comp = this.components[i];
|
|
2232
|
+
|
|
2233
|
+
if (comp.visible) {
|
|
2234
|
+
const adjustedY = isFixedComponent(comp) ? y : y - this.scrollOffset;
|
|
2235
|
+
|
|
2236
|
+
if (comp instanceof Card && comp.clickableChildren && comp.children && comp.children.length > 0) {
|
|
2237
|
+
if (comp.isPointInside(x, adjustedY)) {
|
|
2238
|
+
const cardAdjustedY = adjustedY - comp.y - comp.padding;
|
|
2239
|
+
const cardAdjustedX = x - comp.x - comp.padding;
|
|
2240
|
+
|
|
2241
|
+
for (let j = comp.children.length - 1; j >= 0; j--) {
|
|
2242
|
+
const child = comp.children[j];
|
|
2243
|
+
|
|
2244
|
+
if (child.visible &&
|
|
2245
|
+
cardAdjustedY >= child.y &&
|
|
2246
|
+
cardAdjustedY <= child.y + child.height &&
|
|
2247
|
+
cardAdjustedX >= child.x &&
|
|
2248
|
+
cardAdjustedX <= child.x + child.width) {
|
|
2249
|
+
|
|
2250
|
+
const relativeX = cardAdjustedX - child.x;
|
|
2251
|
+
const relativeY = cardAdjustedY - child.y;
|
|
2252
|
+
|
|
2253
|
+
switch (eventType) {
|
|
2254
|
+
case 'start':
|
|
2255
|
+
child.pressed = true;
|
|
2256
|
+
if (child.onPress) child.onPress?.(relativeX, relativeY);
|
|
2257
|
+
break;
|
|
2258
|
+
|
|
2259
|
+
case 'move':
|
|
2260
|
+
if (!child.hovered) {
|
|
2261
|
+
child.hovered = true;
|
|
2262
|
+
if (child.onHover) child.onHover();
|
|
2263
|
+
}
|
|
2264
|
+
if (child.onMove) child.onMove?.(relativeX, relativeY);
|
|
2265
|
+
break;
|
|
2266
|
+
|
|
2267
|
+
case 'end':
|
|
2268
|
+
if (child.pressed) {
|
|
2269
|
+
child.pressed = false;
|
|
2270
|
+
|
|
2271
|
+
if (child instanceof Input || child instanceof PasswordInput || child instanceof InputTags || child instanceof InputDatalist) {
|
|
2272
|
+
for (let other of this.components) {
|
|
2273
|
+
if (
|
|
2274
|
+
(other instanceof Input ||
|
|
2275
|
+
other instanceof PasswordInput ||
|
|
2276
|
+
other instanceof InputTags ||
|
|
2277
|
+
other instanceof InputDatalist) &&
|
|
2278
|
+
other !== child &&
|
|
2279
|
+
other.focused
|
|
2280
|
+
) {
|
|
2281
|
+
other.focused = false;
|
|
2282
|
+
other.cursorVisible = false;
|
|
2283
|
+
other.onBlur?.();
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
child.focused = true;
|
|
2288
|
+
child.cursorVisible = true;
|
|
2289
|
+
if (child.onFocus) child.onFocus();
|
|
2290
|
+
} else if (child.onClick) {
|
|
2291
|
+
child.onClick();
|
|
2292
|
+
} else if (child.onPress) {
|
|
2293
|
+
child.onPress?.(relativeX, relativeY);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
if (comp.isPointInside(x, adjustedY)) {
|
|
2306
|
+
switch (eventType) {
|
|
2307
|
+
case 'start':
|
|
2308
|
+
comp.pressed = true;
|
|
2309
|
+
if (comp.onPress) comp.onPress(x, adjustedY);
|
|
2310
|
+
break;
|
|
2311
|
+
|
|
2312
|
+
case 'move':
|
|
2313
|
+
if (!comp.hovered) {
|
|
2314
|
+
comp.hovered = true;
|
|
2315
|
+
if (comp.onHover) comp.onHover();
|
|
2316
|
+
}
|
|
2317
|
+
if (comp.onMove) comp.onMove(x, adjustedY);
|
|
2318
|
+
break;
|
|
2319
|
+
|
|
2320
|
+
case 'end':
|
|
2321
|
+
if (comp.pressed) {
|
|
2322
|
+
comp.pressed = false;
|
|
2323
|
+
|
|
2324
|
+
if (comp instanceof Input || comp instanceof PasswordInput || comp instanceof InputTags || comp instanceof InputDatalist) {
|
|
2325
|
+
for (let other of this.components) {
|
|
2326
|
+
if (
|
|
2327
|
+
(other instanceof Input ||
|
|
2328
|
+
other instanceof PasswordInput ||
|
|
2329
|
+
other instanceof InputTags ||
|
|
2330
|
+
other instanceof InputDatalist) &&
|
|
2331
|
+
other !== comp &&
|
|
2332
|
+
other.focused
|
|
2333
|
+
) {
|
|
2334
|
+
other.focused = false;
|
|
2335
|
+
other.cursorVisible = false;
|
|
2336
|
+
other.onBlur?.();
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
comp.focused = true;
|
|
2341
|
+
comp.cursorVisible = true;
|
|
2342
|
+
if (comp.onFocus) comp.onFocus();
|
|
2343
|
+
} else if (comp.onClick) {
|
|
2344
|
+
comp.onClick();
|
|
2345
|
+
} else if (comp.onPress) {
|
|
2346
|
+
comp.onPress(x, adjustedY);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
break;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
return;
|
|
2353
|
+
} else {
|
|
2354
|
+
comp.hovered = false;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
getMaxScroll() {
|
|
2361
|
+
// Utiliser le cache du Worker
|
|
2362
|
+
if (!this._maxScrollDirty && this._cachedMaxScroll !== undefined) {
|
|
2363
|
+
return this._cachedMaxScroll;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// Fallback si le Worker n'est pas encore initialisé
|
|
2367
|
+
let maxY = 0;
|
|
2368
|
+
for (const comp of this.components) {
|
|
2369
|
+
if (this.isFixedComponent(comp) || !comp.visible) continue;
|
|
2370
|
+
const bottom = comp.y + comp.height;
|
|
2371
|
+
if (bottom > maxY) maxY = bottom;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
return Math.max(0, maxY - this.height + 50);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
/*getMaxScroll() {
|
|
2378
|
+
let maxY = 0;
|
|
2379
|
+
for (let comp of this.components) {
|
|
2380
|
+
if (!this.isFixedComponent(comp)) {
|
|
2381
|
+
maxY = Math.max(maxY, comp.y + comp.height);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
return Math.max(0, maxY - this.height + 50);
|
|
2385
|
+
}*/
|
|
2386
|
+
|
|
2387
|
+
handleResize() {
|
|
2388
|
+
if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
|
|
2389
|
+
|
|
2390
|
+
this.resizeTimeout = setTimeout(() => {
|
|
2391
|
+
const newWidth = window.innerWidth;
|
|
2392
|
+
const newHeight = window.innerHeight;
|
|
2393
|
+
|
|
2394
|
+
// Règle clé : on resize UNIQUEMENT si la largeur a vraiment changé
|
|
2395
|
+
// (rotation, split-screen, vraie fenêtre changée)
|
|
2396
|
+
// À la fermeture du clavier → largeur = identique → on skip
|
|
2397
|
+
if (Math.abs(newWidth - this.width) <= 8) { // tolérance pixels (scrollbar, bordures...)
|
|
2398
|
+
console.log("[resize] Largeur identique → probablement clavier (open/close) → ignoré");
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
console.log("[resize] Largeur changée → vrai resize");
|
|
2403
|
+
|
|
2404
|
+
this.width = newWidth;
|
|
2405
|
+
this.height = newHeight;
|
|
2406
|
+
|
|
2407
|
+
this.setupCanvas(); // ← change canvas.width/height + DPR + style
|
|
2408
|
+
this._maxScrollDirty = true;
|
|
2409
|
+
|
|
2410
|
+
if (this.scrollWorker) {
|
|
2411
|
+
this.scrollWorker.postMessage({
|
|
2412
|
+
type: 'UPDATE_DIMENSIONS',
|
|
2413
|
+
payload: { height: this.height }
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
this.components.forEach(comp => comp.onResize?.(this.width, this.height));
|
|
2418
|
+
|
|
2419
|
+
}, 100); // 80–120 ms
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
/*handleResize() {
|
|
2423
|
+
if (this.resizeTimeout) clearTimeout(this.resizeTimeout); // ✅ AJOUTER
|
|
2424
|
+
|
|
2425
|
+
this.resizeTimeout = setTimeout(() => { // ✅ AJOUTER
|
|
2426
|
+
if (!this.useWebGL) {
|
|
2427
|
+
this.width = window.innerWidth;
|
|
2428
|
+
this.height = window.innerHeight;
|
|
2429
|
+
this.setupCanvas();
|
|
2430
|
+
|
|
2431
|
+
for (const comp of this.components) {
|
|
2432
|
+
if (comp._resize) {
|
|
2433
|
+
comp._resize(this.width, this.height);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
this._maxScrollDirty = true; // ✅ AJOUTER
|
|
2437
|
+
}
|
|
2438
|
+
}, 150); // ✅ AJOUTER (throttle 150ms)
|
|
2439
|
+
}*/
|
|
2440
|
+
|
|
2441
|
+
add(component) {
|
|
2442
|
+
this.components.push(component);
|
|
2443
|
+
component._mount();
|
|
2444
|
+
this.updateScrollWorkerComponents();
|
|
2445
|
+
return component;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
remove(component) {
|
|
2449
|
+
const index = this.components.indexOf(component);
|
|
2450
|
+
if (index > -1) {
|
|
2451
|
+
component._unmount();
|
|
2452
|
+
this.components.splice(index, 1);
|
|
2453
|
+
this.updateScrollWorkerComponents();
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// 4. Modifiez markComponentDirty() pour éviter de marquer pendant le scroll
|
|
2458
|
+
markComponentDirty(component) {
|
|
2459
|
+
// Vérifications basiques
|
|
2460
|
+
if (!component || !component.visible) return;
|
|
2461
|
+
|
|
2462
|
+
// ✅ TOUJOURS ajouter au set des composants sales
|
|
2463
|
+
// Les conditions de rendu seront vérifiées dans _renderDirtyComponents
|
|
2464
|
+
if (this.optimizationEnabled) {
|
|
2465
|
+
this.dirtyComponents.add(component);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// ✅ Optionnel : Marquer aussi le composant lui-même
|
|
2469
|
+
if (component._dirty !== undefined) {
|
|
2470
|
+
component._dirty = true;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
enableOptimization() {
|
|
2475
|
+
this.optimizationEnabled = true;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/**
|
|
2479
|
+
* Dessine un petit triangle rouge pour indiquer overflow (style Flutter)
|
|
2480
|
+
*/
|
|
2481
|
+
drawOverflowIndicators() {
|
|
2482
|
+
const ctx = this.ctx;
|
|
2483
|
+
|
|
2484
|
+
// Pour chaque composant
|
|
2485
|
+
for (let comp of this.components) {
|
|
2486
|
+
if (!comp.visible) continue;
|
|
2487
|
+
|
|
2488
|
+
// Position réelle à l'écran
|
|
2489
|
+
const isFixed = this.isFixedComponent(comp);
|
|
2490
|
+
const screenY = isFixed ? comp.y : comp.y + this.scrollOffset;
|
|
2491
|
+
const screenX = comp.x;
|
|
2492
|
+
|
|
2493
|
+
// Vérifier si le composant TEXT a une largeur/hauteur incorrecte
|
|
2494
|
+
let actualWidth = comp.width;
|
|
2495
|
+
let actualHeight = comp.height;
|
|
2496
|
+
|
|
2497
|
+
// Si c'est un Text, vérifier la taille réelle du texte
|
|
2498
|
+
if (comp instanceof Text && comp.text && ctx.measureText) {
|
|
2499
|
+
try {
|
|
2500
|
+
// Sauvegarder le style actuel
|
|
2501
|
+
ctx.save();
|
|
2502
|
+
|
|
2503
|
+
// Appliquer le style du texte
|
|
2504
|
+
if (comp.fontSize) {
|
|
2505
|
+
ctx.font = `${comp.fontSize}px ${comp.fontFamily || 'Arial'}`;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// Mesurer la taille réelle
|
|
2509
|
+
const metrics = ctx.measureText(comp.text);
|
|
2510
|
+
actualWidth = metrics.width + (comp.padding || 0) * 2;
|
|
2511
|
+
actualHeight = (comp.fontSize || 16) + (comp.padding || 0) * 2;
|
|
2512
|
+
|
|
2513
|
+
ctx.restore();
|
|
2514
|
+
} catch (e) {
|
|
2515
|
+
// En cas d'erreur, garder les dimensions par défaut
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// Calculer les limites RÉELLES du composant
|
|
2520
|
+
const compLeft = screenX;
|
|
2521
|
+
const compRight = screenX + actualWidth;
|
|
2522
|
+
const compTop = screenY;
|
|
2523
|
+
const compBottom = screenY + actualHeight;
|
|
2524
|
+
|
|
2525
|
+
// Vérifier les débordements avec les dimensions RÉELLES
|
|
2526
|
+
const overflow = {
|
|
2527
|
+
left: compLeft < 0,
|
|
2528
|
+
right: compRight > this.width,
|
|
2529
|
+
top: compTop < 0,
|
|
2530
|
+
bottom: compBottom > this.height
|
|
2531
|
+
};
|
|
2532
|
+
|
|
2533
|
+
// Si aucun débordement, passer au suivant
|
|
2534
|
+
if (!overflow.left && !overflow.right && !overflow.top && !overflow.bottom) {
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// DEBUG: Afficher les infos du composant
|
|
2539
|
+
if (this.debbug) {
|
|
2540
|
+
console.table({
|
|
2541
|
+
type: comp.constructor?.name,
|
|
2542
|
+
x: comp.x,
|
|
2543
|
+
y: comp.y,
|
|
2544
|
+
declaredSize: `${comp.width}x${comp.height}`,
|
|
2545
|
+
actualSize: `${actualWidth}x${actualHeight}`,
|
|
2546
|
+
screenPos: `(${screenX}, ${screenY})`,
|
|
2547
|
+
overflow
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// Dessiner les indicateurs
|
|
2552
|
+
ctx.save();
|
|
2553
|
+
|
|
2554
|
+
// 1. Bordures rouges sur les parties qui débordent
|
|
2555
|
+
ctx.strokeStyle = 'red';
|
|
2556
|
+
ctx.lineWidth = 2;
|
|
2557
|
+
ctx.fillStyle = 'rgba(255, 0, 0, 0.2)';
|
|
2558
|
+
|
|
2559
|
+
// Gauche
|
|
2560
|
+
if (overflow.left) {
|
|
2561
|
+
const overflowWidth = Math.min(actualWidth, -compLeft);
|
|
2562
|
+
ctx.fillRect(compLeft, compTop, overflowWidth, actualHeight);
|
|
2563
|
+
ctx.strokeRect(compLeft, compTop, overflowWidth, actualHeight);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// Droite
|
|
2567
|
+
if (overflow.right) {
|
|
2568
|
+
const overflowStart = Math.max(0, this.width - compLeft);
|
|
2569
|
+
const overflowWidth = Math.min(actualWidth, compRight - this.width);
|
|
2570
|
+
ctx.fillRect(this.width - overflowWidth, compTop, overflowWidth, actualHeight);
|
|
2571
|
+
ctx.strokeRect(this.width - overflowWidth, compTop, overflowWidth, actualHeight);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// Haut
|
|
2575
|
+
if (overflow.top) {
|
|
2576
|
+
const overflowHeight = Math.min(actualHeight, -compTop);
|
|
2577
|
+
ctx.fillRect(compLeft, compTop, actualWidth, overflowHeight);
|
|
2578
|
+
ctx.strokeRect(compLeft, compTop, actualWidth, overflowHeight);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// Bas
|
|
2582
|
+
if (overflow.bottom) {
|
|
2583
|
+
const overflowStart = Math.max(0, this.height - compTop);
|
|
2584
|
+
const overflowHeight = Math.min(actualHeight, compBottom - this.height);
|
|
2585
|
+
ctx.fillRect(compLeft, this.height - overflowHeight, actualWidth, overflowHeight);
|
|
2586
|
+
ctx.strokeRect(compLeft, this.height - overflowHeight, actualWidth, overflowHeight);
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// 2. Points rouges aux coins
|
|
2590
|
+
ctx.fillStyle = 'red';
|
|
2591
|
+
const markerSize = 6;
|
|
2592
|
+
|
|
2593
|
+
// Coin supérieur gauche
|
|
2594
|
+
if (overflow.left || overflow.top) {
|
|
2595
|
+
ctx.fillRect(compLeft, compTop, markerSize, markerSize);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// Coin supérieur droit
|
|
2599
|
+
if (overflow.right || overflow.top) {
|
|
2600
|
+
ctx.fillRect(compRight - markerSize, compTop, markerSize, markerSize);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// Coin inférieur gauche
|
|
2604
|
+
if (overflow.left || overflow.bottom) {
|
|
2605
|
+
ctx.fillRect(compLeft, compBottom - markerSize, markerSize, markerSize);
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
// Coin inférieur droit
|
|
2609
|
+
if (overflow.right || overflow.bottom) {
|
|
2610
|
+
ctx.fillRect(compRight - markerSize, compBottom - markerSize, markerSize, markerSize);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// 3. Texte d'information (optionnel)
|
|
2614
|
+
if (this.debbug && comp.text) {
|
|
2615
|
+
ctx.fillStyle = 'red';
|
|
2616
|
+
ctx.font = '10px monospace';
|
|
2617
|
+
ctx.textAlign = 'left';
|
|
2618
|
+
|
|
2619
|
+
const overflowText = [];
|
|
2620
|
+
if (overflow.left) overflowText.push('←');
|
|
2621
|
+
if (overflow.right) overflowText.push('→');
|
|
2622
|
+
if (overflow.top) overflowText.push('↑');
|
|
2623
|
+
if (overflow.bottom) overflowText.push('↓');
|
|
2624
|
+
|
|
2625
|
+
if (overflowText.length > 0) {
|
|
2626
|
+
ctx.fillText(
|
|
2627
|
+
`"${comp.text.substring(0, 10)}${comp.text.length > 10 ? '...' : ''}" ${overflowText.join('')}`,
|
|
2628
|
+
compLeft + 5,
|
|
2629
|
+
compTop - 5
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
ctx.restore();
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
startRenderLoop() {
|
|
2639
|
+
let lastScrollOffset = this.scrollOffset;
|
|
2640
|
+
|
|
2641
|
+
const render = () => {
|
|
2642
|
+
if (!this._splashFinished) {
|
|
2643
|
+
requestAnimationFrame(render);
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// TOUJOURS effacer l'écran en premier
|
|
2648
|
+
this.ctx.fillStyle = this.backgroundColor || '#ffffff';
|
|
2649
|
+
this.ctx.fillRect(0, 0, this.width, this.height);
|
|
2650
|
+
|
|
2651
|
+
// Si une transition est en cours
|
|
2652
|
+
if (this.transitionState.isTransitioning) {
|
|
2653
|
+
// Mettre à jour la progression
|
|
2654
|
+
const elapsed = Date.now() - this.transitionState.startTime;
|
|
2655
|
+
this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
|
|
2656
|
+
|
|
2657
|
+
// Rendu spécial pour la transition
|
|
2658
|
+
this.renderSimpleTransition();
|
|
2659
|
+
|
|
2660
|
+
// Si la transition est terminée
|
|
2661
|
+
if (this.transitionState.progress >= 1) {
|
|
2662
|
+
this.transitionState.isTransitioning = false;
|
|
2663
|
+
this.transitionState.oldComponents = [];
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
// Sinon, rendu normal
|
|
2667
|
+
else {
|
|
2668
|
+
this.renderFull();
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// Calcul FPS (optionnel)
|
|
2672
|
+
this._frames++;
|
|
2673
|
+
const now = performance.now();
|
|
2674
|
+
const elapsed = now - this._lastFpsTime;
|
|
2675
|
+
|
|
2676
|
+
if (elapsed >= 1000) {
|
|
2677
|
+
this.fps = Math.round((this._frames * 1000) / elapsed);
|
|
2678
|
+
this._frames = 0;
|
|
2679
|
+
this._lastFpsTime = now;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
if (this.showFps) {
|
|
2683
|
+
this.ctx.save();
|
|
2684
|
+
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
2685
|
+
this.ctx.fillRect(10, 10, 100, 40);
|
|
2686
|
+
this.ctx.fillStyle = '#00ff00';
|
|
2687
|
+
this.ctx.font = 'bold 20px monospace';
|
|
2688
|
+
this.ctx.textAlign = 'left';
|
|
2689
|
+
this.ctx.textBaseline = 'top';
|
|
2690
|
+
this.ctx.fillText(`FPS: ${this.fps}`, 20, 20);
|
|
2691
|
+
this.ctx.restore();
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// Mettre à jour l'inertie du scroll
|
|
2695
|
+
if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
|
|
2696
|
+
this.scrollWorker.postMessage({ type: 'UPDATE_INERTIA' });
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
requestAnimationFrame(render);
|
|
2700
|
+
};
|
|
2701
|
+
|
|
2702
|
+
render();
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
/**
|
|
2706
|
+
* Rendu ultra simple pour la transition slide
|
|
2707
|
+
*/
|
|
2708
|
+
renderSimpleTransition() {
|
|
2709
|
+
const { progress, type, direction, oldComponents, newComponents } = this.transitionState;
|
|
2710
|
+
|
|
2711
|
+
// Easing pour une animation plus fluide
|
|
2712
|
+
const eased = this.easeInOutCubic(progress);
|
|
2713
|
+
|
|
2714
|
+
const directionMultiplier = direction === 'forward' ? 1 : -1;
|
|
2715
|
+
|
|
2716
|
+
if (type === 'slide') {
|
|
2717
|
+
// ✅ AMÉLIORATION 1 : Dessiner l'ANCIENNE vue qui sort
|
|
2718
|
+
this.ctx.save();
|
|
2719
|
+
|
|
2720
|
+
// L'ancienne vue se déplace vers la gauche/droite
|
|
2721
|
+
const oldOffset = -this.width * eased * directionMultiplier;
|
|
2722
|
+
this.ctx.translate(oldOffset, 0);
|
|
2723
|
+
|
|
2724
|
+
// Légère transparence pour donner de la profondeur
|
|
2725
|
+
this.ctx.globalAlpha = 1 - (eased * 0.3);
|
|
2726
|
+
|
|
2727
|
+
// Dessiner les anciens composants
|
|
2728
|
+
for (let comp of oldComponents) {
|
|
2729
|
+
if (comp && comp.visible) {
|
|
2730
|
+
const isFixed = this.isFixedComponent(comp);
|
|
2731
|
+
|
|
2732
|
+
if (isFixed) {
|
|
2733
|
+
comp.draw(this.ctx);
|
|
2734
|
+
} else {
|
|
2735
|
+
this.ctx.save();
|
|
2736
|
+
this.ctx.translate(0, 0); // Pas de scroll pendant la transition
|
|
2737
|
+
comp.draw(this.ctx);
|
|
2738
|
+
this.ctx.restore();
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
this.ctx.restore();
|
|
2744
|
+
|
|
2745
|
+
// ✅ AMÉLIORATION 2 : Dessiner la NOUVELLE vue qui entre
|
|
2746
|
+
this.ctx.save();
|
|
2747
|
+
|
|
2748
|
+
// La nouvelle vue arrive de la droite/gauche
|
|
2749
|
+
const newOffset = this.width * (1 - eased) * directionMultiplier;
|
|
2750
|
+
this.ctx.translate(newOffset, 0);
|
|
2751
|
+
|
|
2752
|
+
// Dessiner les nouveaux composants
|
|
2753
|
+
for (let comp of newComponents) {
|
|
2754
|
+
if (comp && comp.visible) {
|
|
2755
|
+
const isFixed = this.isFixedComponent(comp);
|
|
2756
|
+
|
|
2757
|
+
if (isFixed) {
|
|
2758
|
+
comp.draw(this.ctx);
|
|
2759
|
+
} else {
|
|
2760
|
+
this.ctx.save();
|
|
2761
|
+
this.ctx.translate(0, 0); // Pas de scroll pendant la transition
|
|
2762
|
+
comp.draw(this.ctx);
|
|
2763
|
+
this.ctx.restore();
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
this.ctx.restore();
|
|
2769
|
+
}
|
|
2770
|
+
else if (type === 'fade') {
|
|
2771
|
+
// Ancienne vue qui fade out
|
|
2772
|
+
this.ctx.save();
|
|
2773
|
+
this.ctx.globalAlpha = 1 - eased;
|
|
2774
|
+
|
|
2775
|
+
for (let comp of oldComponents) {
|
|
2776
|
+
if (comp && comp.visible) {
|
|
2777
|
+
const isFixed = this.isFixedComponent(comp);
|
|
2778
|
+
if (isFixed) {
|
|
2779
|
+
comp.draw(this.ctx);
|
|
2780
|
+
} else {
|
|
2781
|
+
this.ctx.save();
|
|
2782
|
+
this.ctx.translate(0, 0);
|
|
2783
|
+
comp.draw(this.ctx);
|
|
2784
|
+
this.ctx.restore();
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
this.ctx.restore();
|
|
2790
|
+
|
|
2791
|
+
// Nouvelle vue qui fade in
|
|
2792
|
+
this.ctx.save();
|
|
2793
|
+
this.ctx.globalAlpha = eased;
|
|
2794
|
+
|
|
2795
|
+
for (let comp of newComponents) {
|
|
2796
|
+
if (comp && comp.visible) {
|
|
2797
|
+
const isFixed = this.isFixedComponent(comp);
|
|
2798
|
+
if (isFixed) {
|
|
2799
|
+
comp.draw(this.ctx);
|
|
2800
|
+
} else {
|
|
2801
|
+
this.ctx.save();
|
|
2802
|
+
this.ctx.translate(0, 0);
|
|
2803
|
+
comp.draw(this.ctx);
|
|
2804
|
+
this.ctx.restore();
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
this.ctx.restore();
|
|
2810
|
+
}
|
|
2811
|
+
else {
|
|
2812
|
+
this.renderFull();
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
/**
|
|
2817
|
+
* Rendu normal (sans transition)
|
|
2818
|
+
*/
|
|
2819
|
+
renderFull() {
|
|
2820
|
+
this.ctx.save();
|
|
2821
|
+
|
|
2822
|
+
// Séparer les composants fixes et scrollables
|
|
2823
|
+
const scrollableComponents = [];
|
|
2824
|
+
const fixedComponents = [];
|
|
2825
|
+
|
|
2826
|
+
for (let comp of this.components) {
|
|
2827
|
+
if (this.isFixedComponent(comp)) {
|
|
2828
|
+
fixedComponents.push(comp);
|
|
2829
|
+
} else {
|
|
2830
|
+
scrollableComponents.push(comp);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// Dessiner les composants scrollables
|
|
2835
|
+
if (scrollableComponents.length > 0) {
|
|
2836
|
+
this.ctx.save();
|
|
2837
|
+
this.ctx.translate(0, this.scrollOffset);
|
|
2838
|
+
|
|
2839
|
+
for (let comp of scrollableComponents) {
|
|
2840
|
+
if (comp.visible) {
|
|
2841
|
+
comp.draw(this.ctx);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
this.ctx.restore();
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
// Dessiner les composants fixes
|
|
2849
|
+
for (let comp of fixedComponents) {
|
|
2850
|
+
if (comp.visible) {
|
|
2851
|
+
comp.draw(this.ctx);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
this.ctx.restore();
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
/**
|
|
2859
|
+
* Mettre à jour la progression de la transition
|
|
2860
|
+
* @private
|
|
2861
|
+
*/
|
|
2862
|
+
updateTransition() {
|
|
2863
|
+
if (!this.transitionState.isTransitioning) return;
|
|
2864
|
+
|
|
2865
|
+
const elapsed = Date.now() - this.transitionState.startTime;
|
|
2866
|
+
this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
|
|
2867
|
+
|
|
2868
|
+
// Si la transition est terminée
|
|
2869
|
+
if (this.transitionState.progress >= 1) {
|
|
2870
|
+
this.transitionState.isTransitioning = false;
|
|
2871
|
+
|
|
2872
|
+
// Marquer tous les nouveaux composants comme sales pour le prochain rendu
|
|
2873
|
+
this.transitionState.newComponents.forEach(comp => {
|
|
2874
|
+
this.markComponentDirty(comp);
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
/**
|
|
2880
|
+
* Rendu complet normal (sans transition)
|
|
2881
|
+
* @private
|
|
2882
|
+
*/
|
|
2883
|
+
renderFull() {
|
|
2884
|
+
// Sauvegarder le contexte
|
|
2885
|
+
this.ctx.save();
|
|
2886
|
+
|
|
2887
|
+
// Séparer les composants
|
|
2888
|
+
const scrollableComponents = [];
|
|
2889
|
+
const fixedComponents = [];
|
|
2890
|
+
|
|
2891
|
+
for (let comp of this.components) {
|
|
2892
|
+
if (this.isFixedComponent(comp)) {
|
|
2893
|
+
fixedComponents.push(comp);
|
|
2894
|
+
} else {
|
|
2895
|
+
scrollableComponents.push(comp);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// Dessiner les composants scrollables avec translation
|
|
2900
|
+
if (scrollableComponents.length > 0) {
|
|
2901
|
+
this.ctx.save();
|
|
2902
|
+
this.ctx.translate(0, this.scrollOffset);
|
|
2903
|
+
|
|
2904
|
+
for (let comp of scrollableComponents) {
|
|
2905
|
+
if (comp.visible) {
|
|
2906
|
+
// Viewport culling
|
|
2907
|
+
const screenY = comp.y + this.scrollOffset;
|
|
2908
|
+
const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
|
|
2909
|
+
|
|
2910
|
+
if (isInViewport) {
|
|
2911
|
+
comp.draw(this.ctx);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
this.ctx.restore();
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
// Dessiner les composants fixes
|
|
2920
|
+
for (let comp of fixedComponents) {
|
|
2921
|
+
if (comp.visible) {
|
|
2922
|
+
comp.draw(this.ctx);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
// Restaurer le contexte
|
|
2927
|
+
this.ctx.restore();
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
|
|
2931
|
+
|
|
2932
|
+
/**
|
|
2933
|
+
* Fait défiler à une position spécifique
|
|
2934
|
+
* @param {number} offset - Position cible
|
|
2935
|
+
* @param {boolean} animated - Avec animation
|
|
2936
|
+
*/
|
|
2937
|
+
scrollTo(offset, animated = true) {
|
|
2938
|
+
if (animated) {
|
|
2939
|
+
const startOffset = this.scrollOffset;
|
|
2940
|
+
const delta = offset - startOffset;
|
|
2941
|
+
const duration = 300;
|
|
2942
|
+
const startTime = performance.now();
|
|
2943
|
+
|
|
2944
|
+
const animate = (currentTime) => {
|
|
2945
|
+
const elapsed = currentTime - startTime;
|
|
2946
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
2947
|
+
const ease = this.easeOutCubic(progress);
|
|
2948
|
+
const currentOffset = startOffset + delta * ease;
|
|
2949
|
+
|
|
2950
|
+
this.scrollWorker.postMessage({
|
|
2951
|
+
type: 'SET_SCROLL_OFFSET',
|
|
2952
|
+
payload: {
|
|
2953
|
+
scrollOffset: currentOffset
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2957
|
+
if (progress < 1) {
|
|
2958
|
+
requestAnimationFrame(animate);
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
|
|
2962
|
+
requestAnimationFrame(animate);
|
|
2963
|
+
} else {
|
|
2964
|
+
this.scrollWorker.postMessage({
|
|
2965
|
+
type: 'SET_SCROLL_OFFSET',
|
|
2966
|
+
payload: {
|
|
2967
|
+
scrollOffset: offset
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
/**
|
|
2974
|
+
* Fonction d'easing
|
|
2975
|
+
*/
|
|
2976
|
+
easeOutCubic(t) {
|
|
2977
|
+
return 1 - Math.pow(1 - t, 3);
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
/**
|
|
2981
|
+
* Nettoie les ressources
|
|
2982
|
+
*/
|
|
2983
|
+
destroy() {
|
|
2984
|
+
if (this.scrollWorker) {
|
|
2985
|
+
this.scrollWorker.terminate();
|
|
2986
|
+
}
|
|
2987
|
+
if (this.worker) {
|
|
2988
|
+
this.worker.terminate();
|
|
2989
|
+
}
|
|
2990
|
+
if (this.logicWorker) {
|
|
2991
|
+
this.logicWorker.terminate();
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
if (this.ctx && typeof this.ctx.destroy === 'function') {
|
|
2995
|
+
this.ctx.destroy();
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
// Nettoyer les écouteurs d'événements
|
|
2999
|
+
this.canvas.removeEventListener('touchstart', this.handleTouchStart);
|
|
3000
|
+
this.canvas.removeEventListener('touchmove', this.handleTouchMove);
|
|
3001
|
+
this.canvas.removeEventListener('touchend', this.handleTouchEnd);
|
|
3002
|
+
this.canvas.removeEventListener('mousedown', this.handleMouseDown);
|
|
3003
|
+
this.canvas.removeEventListener('mousemove', this.handleMouseMove);
|
|
3004
|
+
this.canvas.removeEventListener('mouseup', this.handleMouseUp);
|
|
3005
|
+
window.removeEventListener('resize', this.handleResize);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
// ✅ AJOUTER: Afficher les métriques à l'écran
|
|
3009
|
+
displayMetrics() {
|
|
3010
|
+
const metrics = this.metrics;
|
|
3011
|
+
|
|
3012
|
+
console.table({
|
|
3013
|
+
'⚙️ Initialisation Framework': `${metrics.initTime.toFixed(2)}ms`,
|
|
3014
|
+
'🎨 Premier Rendu': `${metrics.firstRenderTime.toFixed(2)}ms`,
|
|
3015
|
+
'🚀 Temps Total Startup': `${metrics.totalStartupTime.toFixed(2)}ms`,
|
|
3016
|
+
'📊 FPS Actuel': this.fps
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// ✅ AJOUTER: Obtenir les métriques
|
|
3021
|
+
getMetrics() {
|
|
3022
|
+
return {
|
|
3023
|
+
...this.metrics,
|
|
3024
|
+
currentFPS: this.fps,
|
|
3025
|
+
componentsCount: this.components.length
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
isFixedComponent(comp) {
|
|
3030
|
+
return FIXED_COMPONENT_TYPES.has(comp.constructor);
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
showToast(message, duration = 3000) {
|
|
3034
|
+
const toast = new Toast(this, {
|
|
3035
|
+
text: message,
|
|
3036
|
+
duration: duration,
|
|
3037
|
+
x: this.width / 2,
|
|
3038
|
+
y: this.height - 100
|
|
3039
|
+
});
|
|
3040
|
+
this.add(toast);
|
|
3041
|
+
toast.show();
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
export default CanvasFramework;
|