canvasframework 0.5.56 → 0.5.58
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/components/QRCodeGenerator.js +207 -131
- package/core/CanvasFramework.js +273 -94
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* QRCodeGenerator SANS librairie externe
|
|
5
|
+
* Version simplifiée qui fonctionne pour texte court et URLs
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* new QRCodeGenerator(framework, {
|
|
@@ -18,158 +18,243 @@ class QRCodeGenerator extends Component {
|
|
|
18
18
|
super(framework, options);
|
|
19
19
|
|
|
20
20
|
// Données du QR code
|
|
21
|
-
this.data = options.data || options.text || '
|
|
21
|
+
this.data = options.data || options.text || 'Hello';
|
|
22
22
|
|
|
23
23
|
// Dimensions
|
|
24
24
|
this.width = options.width || 250;
|
|
25
25
|
this.height = options.height || 250;
|
|
26
|
-
|
|
27
|
-
// Le QR code est toujours carré, on prend la plus petite dimension
|
|
28
26
|
this.qrSize = Math.min(this.width, this.height);
|
|
29
27
|
|
|
30
|
-
// Options
|
|
31
|
-
this.errorCorrectionLevel = options.errorCorrectionLevel || 'M';
|
|
28
|
+
// Options visuelles
|
|
32
29
|
this.backgroundColor = options.backgroundColor || '#ffffff';
|
|
33
30
|
this.foregroundColor = options.foregroundColor || '#000000';
|
|
34
|
-
this.
|
|
31
|
+
this.padding = options.padding || 20;
|
|
35
32
|
|
|
36
|
-
// État
|
|
37
|
-
this.
|
|
38
|
-
this.
|
|
39
|
-
this.isGenerating = false;
|
|
40
|
-
this.error = null;
|
|
33
|
+
// État
|
|
34
|
+
this.qrMatrix = null;
|
|
35
|
+
this.moduleSize = 0;
|
|
41
36
|
|
|
42
|
-
//
|
|
43
|
-
this.
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Charge la librairie qrcode.js depuis CDN
|
|
48
|
-
*/
|
|
49
|
-
loadQRCodeLibrary() {
|
|
50
|
-
if (typeof QRCode === 'undefined') {
|
|
51
|
-
const script = document.createElement('script');
|
|
52
|
-
script.src = 'https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js';
|
|
53
|
-
script.onload = () => {
|
|
54
|
-
console.log('[QRCodeGenerator] Library loaded');
|
|
55
|
-
this.generateQRCode();
|
|
56
|
-
};
|
|
57
|
-
script.onerror = () => {
|
|
58
|
-
console.error('[QRCodeGenerator] Failed to load library');
|
|
59
|
-
this.error = 'Échec du chargement';
|
|
60
|
-
this.markDirty();
|
|
61
|
-
};
|
|
62
|
-
document.head.appendChild(script);
|
|
63
|
-
} else {
|
|
64
|
-
// Librairie déjà chargée
|
|
65
|
-
this.generateQRCode();
|
|
66
|
-
}
|
|
37
|
+
// Générer immédiatement
|
|
38
|
+
this.generateQRCode();
|
|
67
39
|
}
|
|
68
40
|
|
|
69
41
|
async _mount() {
|
|
70
42
|
super._mount?.();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (this.data && typeof QRCode !== 'undefined' && !this.qrCanvas) {
|
|
74
|
-
await this.generateQRCode();
|
|
43
|
+
if (!this.qrMatrix) {
|
|
44
|
+
this.generateQRCode();
|
|
75
45
|
}
|
|
76
46
|
}
|
|
77
47
|
|
|
78
48
|
destroy() {
|
|
79
|
-
|
|
80
|
-
this.qrCanvas = null;
|
|
81
|
-
}
|
|
82
|
-
if (this.qrImage) {
|
|
83
|
-
this.qrImage = null;
|
|
84
|
-
}
|
|
49
|
+
this.qrMatrix = null;
|
|
85
50
|
super.destroy?.();
|
|
86
51
|
}
|
|
87
52
|
|
|
88
53
|
onUnmount() {
|
|
89
54
|
console.log('[QRCodeGenerator] onUnmount called');
|
|
90
|
-
|
|
91
|
-
this.qrCanvas = null;
|
|
92
|
-
}
|
|
93
|
-
if (this.qrImage) {
|
|
94
|
-
this.qrImage = null;
|
|
95
|
-
}
|
|
55
|
+
this.qrMatrix = null;
|
|
96
56
|
}
|
|
97
57
|
|
|
98
58
|
/**
|
|
99
|
-
* Génère
|
|
59
|
+
* Génère un QR code simple (Version 1 : 21x21)
|
|
100
60
|
*/
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
61
|
+
generateQRCode() {
|
|
62
|
+
try {
|
|
63
|
+
// Taille fixe pour Version 1 (supporte ~25 caractères)
|
|
64
|
+
const size = 21;
|
|
65
|
+
|
|
66
|
+
// Créer la matrice
|
|
67
|
+
this.qrMatrix = this.createMatrix(size);
|
|
68
|
+
|
|
69
|
+
// Ajouter les patterns de base
|
|
70
|
+
this.addFinderPatterns(this.qrMatrix);
|
|
71
|
+
this.addTimingPatterns(this.qrMatrix);
|
|
72
|
+
this.addAlignmentPattern(this.qrMatrix);
|
|
73
|
+
|
|
74
|
+
// Ajouter les données (simplifié)
|
|
75
|
+
this.addDataSimple(this.qrMatrix);
|
|
76
|
+
|
|
77
|
+
// Calculer la taille d'un module
|
|
78
|
+
const availableSize = this.qrSize - (this.padding * 2);
|
|
79
|
+
this.moduleSize = Math.floor(availableSize / size);
|
|
80
|
+
|
|
81
|
+
console.log('[QRCodeGenerator] QR Code generated:', size + 'x' + size);
|
|
104
82
|
this.markDirty();
|
|
105
|
-
|
|
83
|
+
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error('[QRCodeGenerator] Error:', err);
|
|
106
86
|
}
|
|
87
|
+
}
|
|
107
88
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Crée une matrice vide
|
|
91
|
+
*/
|
|
92
|
+
createMatrix(size) {
|
|
93
|
+
const matrix = [];
|
|
94
|
+
for (let i = 0; i < size; i++) {
|
|
95
|
+
matrix[i] = new Array(size).fill(-1); // -1 = non défini
|
|
112
96
|
}
|
|
97
|
+
return matrix;
|
|
98
|
+
}
|
|
113
99
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Ajoute les patterns de position (3 carrés dans les coins)
|
|
102
|
+
*/
|
|
103
|
+
addFinderPatterns(matrix) {
|
|
104
|
+
const size = matrix.length;
|
|
105
|
+
|
|
106
|
+
// Pattern 7x7
|
|
107
|
+
const addFinder = (startX, startY) => {
|
|
108
|
+
for (let dy = 0; dy < 7; dy++) {
|
|
109
|
+
for (let dx = 0; dx < 7; dx++) {
|
|
110
|
+
const x = startX + dx;
|
|
111
|
+
const y = startY + dy;
|
|
112
|
+
|
|
113
|
+
// Carré extérieur (7x7)
|
|
114
|
+
if (dx === 0 || dx === 6 || dy === 0 || dy === 6) {
|
|
115
|
+
matrix[y][x] = 1;
|
|
116
|
+
}
|
|
117
|
+
// Carré intérieur (3x3) centré
|
|
118
|
+
else if (dx >= 2 && dx <= 4 && dy >= 2 && dy <= 4) {
|
|
119
|
+
matrix[y][x] = 1;
|
|
120
|
+
}
|
|
121
|
+
// Blanc entre les deux
|
|
122
|
+
else {
|
|
123
|
+
matrix[y][x] = 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
121
127
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
// Séparateurs blancs (1 pixel autour)
|
|
129
|
+
for (let i = 0; i < 8; i++) {
|
|
130
|
+
if (startX === 0 && startY === 0) {
|
|
131
|
+
// Haut gauche
|
|
132
|
+
if (i < 7) matrix[7][i] = 0;
|
|
133
|
+
if (i < 7) matrix[i][7] = 0;
|
|
134
|
+
} else if (startX === size - 7 && startY === 0) {
|
|
135
|
+
// Haut droit
|
|
136
|
+
if (i < 7) matrix[7][size - 8 + i] = 0;
|
|
137
|
+
if (i < 7) matrix[i][size - 8] = 0;
|
|
138
|
+
} else if (startX === 0 && startY === size - 7) {
|
|
139
|
+
// Bas gauche
|
|
140
|
+
if (i < 7) matrix[size - 8][i] = 0;
|
|
141
|
+
if (i < 7) matrix[size - 8 + i][7] = 0;
|
|
130
142
|
}
|
|
131
|
-
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Haut gauche
|
|
147
|
+
addFinder(0, 0);
|
|
148
|
+
|
|
149
|
+
// Haut droit
|
|
150
|
+
addFinder(size - 7, 0);
|
|
151
|
+
|
|
152
|
+
// Bas gauche
|
|
153
|
+
addFinder(0, size - 7);
|
|
154
|
+
}
|
|
132
155
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Ajoute les lignes de timing
|
|
158
|
+
*/
|
|
159
|
+
addTimingPatterns(matrix) {
|
|
160
|
+
const size = matrix.length;
|
|
161
|
+
|
|
162
|
+
// Ligne horizontale (ligne 6)
|
|
163
|
+
for (let i = 8; i < size - 8; i++) {
|
|
164
|
+
matrix[6][i] = (i % 2 === 0) ? 1 : 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Ligne verticale (colonne 6)
|
|
168
|
+
for (let i = 8; i < size - 8; i++) {
|
|
169
|
+
matrix[i][6] = (i % 2 === 0) ? 1 : 0;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
146
172
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Ajoute le pattern d'alignement (pour Version 1, pas nécessaire, mais on le met quand même)
|
|
175
|
+
*/
|
|
176
|
+
addAlignmentPattern(matrix) {
|
|
177
|
+
// Pour Version 1, pas d'alignment pattern
|
|
178
|
+
// On remplit juste avec le module noir obligatoire
|
|
179
|
+
const size = matrix.length;
|
|
180
|
+
matrix[size - 8][8] = 1; // Module noir fixe
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Ajoute les données de manière simplifiée (pattern basé sur les données)
|
|
185
|
+
*/
|
|
186
|
+
addDataSimple(matrix) {
|
|
187
|
+
const size = matrix.length;
|
|
188
|
+
|
|
189
|
+
// Convertir les données en une séquence de bits basique
|
|
190
|
+
let bits = '';
|
|
191
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
192
|
+
const charCode = this.data.charCodeAt(i);
|
|
193
|
+
// Convertir en binaire sur 8 bits
|
|
194
|
+
bits += charCode.toString(2).padStart(8, '0');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Ajouter un padding si nécessaire
|
|
198
|
+
while (bits.length < 100) {
|
|
199
|
+
bits += '0';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let bitIndex = 0;
|
|
203
|
+
|
|
204
|
+
// Remplir la matrice en zigzag (de droite à gauche, de bas en haut)
|
|
205
|
+
for (let col = size - 1; col > 0; col -= 2) {
|
|
206
|
+
// Sauter la colonne de timing
|
|
207
|
+
if (col === 6) col--;
|
|
208
|
+
|
|
209
|
+
for (let row = 0; row < size; row++) {
|
|
210
|
+
// Alterner direction : bas->haut puis haut->bas
|
|
211
|
+
const y = ((col + 1) % 4 < 2) ? (size - 1 - row) : row;
|
|
212
|
+
|
|
213
|
+
// Remplir 2 colonnes à la fois
|
|
214
|
+
for (let c = 0; c < 2; c++) {
|
|
215
|
+
const x = col - c;
|
|
216
|
+
|
|
217
|
+
// Ne pas écraser les patterns existants
|
|
218
|
+
if (matrix[y][x] !== -1) continue;
|
|
219
|
+
|
|
220
|
+
// Placer un bit
|
|
221
|
+
if (bitIndex < bits.length) {
|
|
222
|
+
matrix[y][x] = bits[bitIndex] === '1' ? 1 : 0;
|
|
223
|
+
bitIndex++;
|
|
224
|
+
} else {
|
|
225
|
+
matrix[y][x] = 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Remplir les cases restantes avec 0
|
|
232
|
+
for (let y = 0; y < size; y++) {
|
|
233
|
+
for (let x = 0; x < size; x++) {
|
|
234
|
+
if (matrix[y][x] === -1) {
|
|
235
|
+
matrix[y][x] = 0;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
152
238
|
}
|
|
153
239
|
}
|
|
154
240
|
|
|
155
241
|
/**
|
|
156
|
-
* Change les données et régénère
|
|
242
|
+
* Change les données et régénère
|
|
157
243
|
*/
|
|
158
244
|
async setData(newData) {
|
|
159
245
|
if (this.data === newData) return;
|
|
160
|
-
|
|
161
246
|
this.data = newData;
|
|
162
|
-
|
|
247
|
+
this.generateQRCode();
|
|
163
248
|
}
|
|
164
249
|
|
|
165
250
|
/**
|
|
166
|
-
* Change la taille
|
|
251
|
+
* Change la taille
|
|
167
252
|
*/
|
|
168
253
|
async setSize(newSize) {
|
|
169
254
|
this.width = newSize;
|
|
170
255
|
this.height = newSize;
|
|
171
256
|
this.qrSize = newSize;
|
|
172
|
-
|
|
257
|
+
this.generateQRCode();
|
|
173
258
|
}
|
|
174
259
|
|
|
175
260
|
draw(ctx) {
|
|
@@ -179,12 +264,8 @@ class QRCodeGenerator extends Component {
|
|
|
179
264
|
ctx.fillStyle = this.backgroundColor;
|
|
180
265
|
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
181
266
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const offsetY = (this.height - this.qrSize) / 2;
|
|
185
|
-
|
|
186
|
-
// État : Génération en cours
|
|
187
|
-
if (this.isGenerating) {
|
|
267
|
+
if (!this.qrMatrix || this.moduleSize === 0) {
|
|
268
|
+
// Message de chargement
|
|
188
269
|
ctx.fillStyle = '#666';
|
|
189
270
|
ctx.font = '14px Arial';
|
|
190
271
|
ctx.textAlign = 'center';
|
|
@@ -198,30 +279,25 @@ class QRCodeGenerator extends Component {
|
|
|
198
279
|
return;
|
|
199
280
|
}
|
|
200
281
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
ctx.textAlign = 'center';
|
|
206
|
-
ctx.textBaseline = 'middle';
|
|
207
|
-
ctx.fillText(
|
|
208
|
-
this.error,
|
|
209
|
-
this.x + this.width / 2,
|
|
210
|
-
this.y + this.height / 2
|
|
211
|
-
);
|
|
212
|
-
ctx.restore();
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
282
|
+
// Centrer le QR code
|
|
283
|
+
const qrPixelSize = this.qrMatrix.length * this.moduleSize;
|
|
284
|
+
const offsetX = (this.width - qrPixelSize) / 2;
|
|
285
|
+
const offsetY = (this.height - qrPixelSize) / 2;
|
|
215
286
|
|
|
216
|
-
// Dessiner
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
this.y
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
287
|
+
// Dessiner les modules
|
|
288
|
+
ctx.fillStyle = this.foregroundColor;
|
|
289
|
+
|
|
290
|
+
for (let y = 0; y < this.qrMatrix.length; y++) {
|
|
291
|
+
for (let x = 0; x < this.qrMatrix[y].length; x++) {
|
|
292
|
+
if (this.qrMatrix[y][x] === 1) {
|
|
293
|
+
ctx.fillRect(
|
|
294
|
+
this.x + offsetX + x * this.moduleSize,
|
|
295
|
+
this.y + offsetY + y * this.moduleSize,
|
|
296
|
+
this.moduleSize,
|
|
297
|
+
this.moduleSize
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
225
301
|
}
|
|
226
302
|
|
|
227
303
|
ctx.restore();
|
package/core/CanvasFramework.js
CHANGED
|
@@ -374,7 +374,12 @@ class CanvasFramework {
|
|
|
374
374
|
};
|
|
375
375
|
this._firstRenderDone = false;
|
|
376
376
|
this._startupStartTime = startTime;
|
|
377
|
+
// Dans le constructeur, après this.scrollFriction = 0.95;
|
|
378
|
+
this.scrollFriction = 0.95;
|
|
377
379
|
|
|
380
|
+
// ✅ AJOUTER
|
|
381
|
+
this.overscrollDistance = 0;
|
|
382
|
+
this.isOverscrollAnimating = false;
|
|
378
383
|
// ✅ Créer automatiquement le canvas
|
|
379
384
|
this.canvas = document.createElement('canvas');
|
|
380
385
|
this.canvas.id = canvasId || `canvas-${Date.now()}`;
|
|
@@ -620,6 +625,7 @@ class CanvasFramework {
|
|
|
620
625
|
if (this.optimizations.useSpatialPartitioning) {
|
|
621
626
|
this._initSpatialPartitioning();
|
|
622
627
|
}
|
|
628
|
+
|
|
623
629
|
}
|
|
624
630
|
|
|
625
631
|
/**
|
|
@@ -688,7 +694,7 @@ class CanvasFramework {
|
|
|
688
694
|
* Crée le Worker pour le calcul du scroll
|
|
689
695
|
*/
|
|
690
696
|
createScrollWorker() {
|
|
691
|
-
|
|
697
|
+
const workerCode = `
|
|
692
698
|
let state = {
|
|
693
699
|
scrollOffset: 0,
|
|
694
700
|
scrollVelocity: 0,
|
|
@@ -697,7 +703,10 @@ class CanvasFramework {
|
|
|
697
703
|
maxScroll: 0,
|
|
698
704
|
height: 0,
|
|
699
705
|
lastTouchY: 0,
|
|
700
|
-
components: []
|
|
706
|
+
components: [],
|
|
707
|
+
overscrollDistance: 0,
|
|
708
|
+
maxOverscroll: 150, // ✅ Limite maximale
|
|
709
|
+
overscrollResistance: 0.3
|
|
701
710
|
};
|
|
702
711
|
|
|
703
712
|
const FIXED_COMPONENT_TYPES = [
|
|
@@ -708,13 +717,11 @@ class CanvasFramework {
|
|
|
708
717
|
|
|
709
718
|
const calculateMaxScroll = () => {
|
|
710
719
|
let maxY = 0;
|
|
711
|
-
|
|
712
720
|
for (const comp of state.components) {
|
|
713
721
|
if (FIXED_COMPONENT_TYPES.includes(comp.type) || !comp.visible) continue;
|
|
714
722
|
const bottom = comp.y + comp.height;
|
|
715
723
|
if (bottom > maxY) maxY = bottom;
|
|
716
724
|
}
|
|
717
|
-
|
|
718
725
|
return Math.max(0, maxY - state.height + 50);
|
|
719
726
|
};
|
|
720
727
|
|
|
@@ -722,7 +729,29 @@ class CanvasFramework {
|
|
|
722
729
|
if (Math.abs(state.scrollVelocity) > 0.1 && !state.isDragging) {
|
|
723
730
|
state.scrollOffset += state.scrollVelocity;
|
|
724
731
|
state.maxScroll = calculateMaxScroll();
|
|
725
|
-
|
|
732
|
+
|
|
733
|
+
// ✅ CORRIGER: Limiter l'overscroll pendant l'inertie
|
|
734
|
+
if (state.scrollOffset > 0) {
|
|
735
|
+
// Limiter à maxOverscroll
|
|
736
|
+
if (state.scrollOffset > state.maxOverscroll) {
|
|
737
|
+
state.scrollOffset = state.maxOverscroll;
|
|
738
|
+
state.scrollVelocity = 0;
|
|
739
|
+
}
|
|
740
|
+
state.overscrollDistance = state.scrollOffset;
|
|
741
|
+
state.scrollVelocity *= 0.85;
|
|
742
|
+
} else if (state.scrollOffset < -state.maxScroll) {
|
|
743
|
+
const excess = Math.abs(state.scrollOffset + state.maxScroll);
|
|
744
|
+
// Limiter à maxOverscroll
|
|
745
|
+
if (excess > state.maxOverscroll) {
|
|
746
|
+
state.scrollOffset = -state.maxScroll - state.maxOverscroll;
|
|
747
|
+
state.scrollVelocity = 0;
|
|
748
|
+
}
|
|
749
|
+
state.overscrollDistance = state.scrollOffset + state.maxScroll;
|
|
750
|
+
state.scrollVelocity *= 0.85;
|
|
751
|
+
} else {
|
|
752
|
+
state.overscrollDistance = 0;
|
|
753
|
+
}
|
|
754
|
+
|
|
726
755
|
state.scrollVelocity *= state.scrollFriction;
|
|
727
756
|
} else {
|
|
728
757
|
state.scrollVelocity = 0;
|
|
@@ -731,20 +760,82 @@ class CanvasFramework {
|
|
|
731
760
|
return {
|
|
732
761
|
scrollOffset: state.scrollOffset,
|
|
733
762
|
scrollVelocity: state.scrollVelocity,
|
|
734
|
-
maxScroll: state.maxScroll
|
|
763
|
+
maxScroll: state.maxScroll,
|
|
764
|
+
overscrollDistance: state.overscrollDistance
|
|
735
765
|
};
|
|
736
766
|
};
|
|
737
767
|
|
|
738
768
|
const handleTouchMove = (deltaY) => {
|
|
739
769
|
if (state.isDragging) {
|
|
740
|
-
state.scrollOffset += deltaY;
|
|
741
770
|
state.maxScroll = calculateMaxScroll();
|
|
742
|
-
|
|
771
|
+
const wouldBeOffset = state.scrollOffset + deltaY;
|
|
772
|
+
|
|
773
|
+
let actualDelta = deltaY;
|
|
774
|
+
|
|
775
|
+
// ✅ CORRIGER: Appliquer les limites d'overscroll
|
|
776
|
+
if (wouldBeOffset > 0) {
|
|
777
|
+
// Overscroll en haut
|
|
778
|
+
const currentOverscroll = state.scrollOffset > 0 ? state.scrollOffset : 0;
|
|
779
|
+
|
|
780
|
+
// Si on a déjà atteint la limite, ne plus bouger
|
|
781
|
+
if (currentOverscroll >= state.maxOverscroll) {
|
|
782
|
+
actualDelta = 0;
|
|
783
|
+
state.overscrollDistance = state.maxOverscroll;
|
|
784
|
+
} else {
|
|
785
|
+
// Calculer la résistance progressive
|
|
786
|
+
const overscrollRatio = currentOverscroll / state.maxOverscroll;
|
|
787
|
+
const resistance = state.overscrollResistance * (1 - overscrollRatio * 0.7);
|
|
788
|
+
|
|
789
|
+
actualDelta = deltaY * resistance;
|
|
790
|
+
|
|
791
|
+
// S'assurer qu'on ne dépasse pas la limite
|
|
792
|
+
const newOverscroll = currentOverscroll + actualDelta;
|
|
793
|
+
if (newOverscroll > state.maxOverscroll) {
|
|
794
|
+
actualDelta = state.maxOverscroll - currentOverscroll;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
state.overscrollDistance = currentOverscroll + actualDelta;
|
|
798
|
+
}
|
|
799
|
+
} else if (wouldBeOffset < -state.maxScroll) {
|
|
800
|
+
// Overscroll en bas
|
|
801
|
+
const currentOverscroll = state.scrollOffset < -state.maxScroll
|
|
802
|
+
? Math.abs(state.scrollOffset + state.maxScroll)
|
|
803
|
+
: 0;
|
|
804
|
+
|
|
805
|
+
// Si on a déjà atteint la limite, ne plus bouger
|
|
806
|
+
if (currentOverscroll >= state.maxOverscroll) {
|
|
807
|
+
actualDelta = 0;
|
|
808
|
+
state.overscrollDistance = -state.maxOverscroll;
|
|
809
|
+
} else {
|
|
810
|
+
// Calculer la résistance progressive
|
|
811
|
+
const overscrollRatio = currentOverscroll / state.maxOverscroll;
|
|
812
|
+
const resistance = state.overscrollResistance * (1 - overscrollRatio * 0.7);
|
|
813
|
+
|
|
814
|
+
actualDelta = deltaY * resistance;
|
|
815
|
+
|
|
816
|
+
// S'assurer qu'on ne dépasse pas la limite
|
|
817
|
+
const newOverscroll = currentOverscroll + Math.abs(actualDelta);
|
|
818
|
+
if (newOverscroll > state.maxOverscroll) {
|
|
819
|
+
actualDelta = deltaY > 0
|
|
820
|
+
? state.maxOverscroll - currentOverscroll
|
|
821
|
+
: -(state.maxOverscroll - currentOverscroll);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
state.overscrollDistance = -(currentOverscroll + Math.abs(actualDelta));
|
|
825
|
+
}
|
|
826
|
+
} else {
|
|
827
|
+
// Scroll normal
|
|
828
|
+
state.overscrollDistance = 0;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
state.scrollOffset += actualDelta;
|
|
743
832
|
state.scrollVelocity = deltaY;
|
|
833
|
+
|
|
744
834
|
return {
|
|
745
835
|
scrollOffset: state.scrollOffset,
|
|
746
836
|
scrollVelocity: state.scrollVelocity,
|
|
747
|
-
maxScroll: state.maxScroll
|
|
837
|
+
maxScroll: state.maxScroll,
|
|
838
|
+
overscrollDistance: state.overscrollDistance
|
|
748
839
|
};
|
|
749
840
|
}
|
|
750
841
|
return null;
|
|
@@ -755,16 +846,14 @@ class CanvasFramework {
|
|
|
755
846
|
|
|
756
847
|
switch (type) {
|
|
757
848
|
case 'INIT':
|
|
758
|
-
state = {
|
|
759
|
-
...state,
|
|
760
|
-
...payload
|
|
761
|
-
};
|
|
849
|
+
state = { ...state, ...payload };
|
|
762
850
|
state.maxScroll = calculateMaxScroll();
|
|
763
851
|
self.postMessage({
|
|
764
852
|
type: 'INITIALIZED',
|
|
765
853
|
payload: {
|
|
766
854
|
scrollOffset: state.scrollOffset,
|
|
767
|
-
maxScroll: state.maxScroll
|
|
855
|
+
maxScroll: state.maxScroll,
|
|
856
|
+
overscrollDistance: 0
|
|
768
857
|
}
|
|
769
858
|
});
|
|
770
859
|
break;
|
|
@@ -817,12 +906,43 @@ class CanvasFramework {
|
|
|
817
906
|
case 'SET_SCROLL_OFFSET':
|
|
818
907
|
state.scrollOffset = payload.scrollOffset;
|
|
819
908
|
state.maxScroll = calculateMaxScroll();
|
|
820
|
-
state.
|
|
909
|
+
state.overscrollDistance = 0;
|
|
910
|
+
self.postMessage({
|
|
911
|
+
type: 'SCROLL_UPDATED',
|
|
912
|
+
payload: {
|
|
913
|
+
scrollOffset: state.scrollOffset,
|
|
914
|
+
maxScroll: state.maxScroll,
|
|
915
|
+
overscrollDistance: 0
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
break;
|
|
919
|
+
|
|
920
|
+
case 'ANIMATE_RETURN':
|
|
921
|
+
state.maxScroll = calculateMaxScroll();
|
|
922
|
+
|
|
923
|
+
if (state.scrollOffset > 0) {
|
|
924
|
+
state.scrollOffset *= 0.75;
|
|
925
|
+
state.overscrollDistance = state.scrollOffset;
|
|
926
|
+
} else if (state.scrollOffset < -state.maxScroll) {
|
|
927
|
+
const diff = state.scrollOffset + state.maxScroll;
|
|
928
|
+
state.scrollOffset = -state.maxScroll + (diff * 0.75);
|
|
929
|
+
state.overscrollDistance = diff * 0.75;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const shouldContinue = Math.abs(state.overscrollDistance) > 1;
|
|
933
|
+
|
|
934
|
+
if (!shouldContinue) {
|
|
935
|
+
state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
|
|
936
|
+
state.overscrollDistance = 0;
|
|
937
|
+
}
|
|
938
|
+
|
|
821
939
|
self.postMessage({
|
|
822
940
|
type: 'SCROLL_UPDATED',
|
|
823
941
|
payload: {
|
|
824
942
|
scrollOffset: state.scrollOffset,
|
|
825
|
-
maxScroll: state.maxScroll
|
|
943
|
+
maxScroll: state.maxScroll,
|
|
944
|
+
overscrollDistance: state.overscrollDistance,
|
|
945
|
+
shouldContinue
|
|
826
946
|
}
|
|
827
947
|
});
|
|
828
948
|
break;
|
|
@@ -834,7 +954,8 @@ class CanvasFramework {
|
|
|
834
954
|
scrollOffset: state.scrollOffset,
|
|
835
955
|
scrollVelocity: state.scrollVelocity,
|
|
836
956
|
maxScroll: state.maxScroll,
|
|
837
|
-
isDragging: state.isDragging
|
|
957
|
+
isDragging: state.isDragging,
|
|
958
|
+
overscrollDistance: state.overscrollDistance
|
|
838
959
|
}
|
|
839
960
|
});
|
|
840
961
|
break;
|
|
@@ -842,64 +963,71 @@ class CanvasFramework {
|
|
|
842
963
|
};
|
|
843
964
|
`;
|
|
844
965
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
return new Worker(URL.createObjectURL(blob));
|
|
849
|
-
}
|
|
966
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
967
|
+
return new Worker(URL.createObjectURL(blob));
|
|
968
|
+
}
|
|
850
969
|
|
|
851
970
|
/**
|
|
852
971
|
* Gère les messages du Scroll Worker
|
|
853
972
|
*/
|
|
854
973
|
handleScrollWorkerMessage(e) {
|
|
855
|
-
|
|
856
|
-
type,
|
|
857
|
-
payload
|
|
858
|
-
} = e.data;
|
|
974
|
+
const { type, payload } = e.data;
|
|
859
975
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
this.
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
this.components.forEach(comp => {
|
|
875
|
-
if (!this.isFixedComponent(comp)) {
|
|
876
|
-
this.markComponentDirty(comp);
|
|
877
|
-
}
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
break;
|
|
976
|
+
switch (type) {
|
|
977
|
+
case 'SCROLL_UPDATED':
|
|
978
|
+
this.scrollOffset = payload.scrollOffset;
|
|
979
|
+
this.scrollVelocity = payload.scrollVelocity;
|
|
980
|
+
|
|
981
|
+
// ✅ AJOUTER
|
|
982
|
+
this.overscrollDistance = payload.overscrollDistance || 0;
|
|
983
|
+
|
|
984
|
+
if (Math.abs(payload.scrollVelocity) > 0.5) {
|
|
985
|
+
this.dirtyComponents.clear();
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
989
|
+
this._maxScrollDirty = false;
|
|
881
990
|
|
|
882
|
-
|
|
883
|
-
this.
|
|
884
|
-
|
|
885
|
-
|
|
991
|
+
if (Math.abs(payload.scrollVelocity) > 0) {
|
|
992
|
+
this.components.forEach(comp => {
|
|
993
|
+
if (!this.isFixedComponent(comp)) {
|
|
994
|
+
this.markComponentDirty(comp);
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// ✅ AJOUTER: Continuer l'animation de retour si nécessaire
|
|
1000
|
+
if (payload.shouldContinue !== undefined && payload.shouldContinue) {
|
|
1001
|
+
requestAnimationFrame(() => {
|
|
1002
|
+
this.scrollWorker.postMessage({ type: 'ANIMATE_RETURN' });
|
|
1003
|
+
});
|
|
1004
|
+
} else if (payload.shouldContinue === false) {
|
|
1005
|
+
this.isOverscrollAnimating = false;
|
|
1006
|
+
}
|
|
1007
|
+
break;
|
|
886
1008
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
break;
|
|
1009
|
+
case 'MAX_SCROLL_UPDATED':
|
|
1010
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
1011
|
+
this._maxScrollDirty = false;
|
|
1012
|
+
break;
|
|
892
1013
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1014
|
+
case 'INITIALIZED':
|
|
1015
|
+
this.scrollOffset = payload.scrollOffset;
|
|
1016
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
1017
|
+
this._maxScrollDirty = false;
|
|
1018
|
+
this.overscrollDistance = 0; // ✅ AJOUTER
|
|
1019
|
+
break;
|
|
1020
|
+
|
|
1021
|
+
case 'STATE':
|
|
1022
|
+
this.scrollOffset = payload.scrollOffset;
|
|
1023
|
+
this.scrollVelocity = payload.scrollVelocity;
|
|
1024
|
+
this.isDragging = payload.isDragging;
|
|
1025
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
1026
|
+
this._maxScrollDirty = false;
|
|
1027
|
+
this.overscrollDistance = payload.overscrollDistance || 0; // ✅ AJOUTER
|
|
1028
|
+
break;
|
|
902
1029
|
}
|
|
1030
|
+
}
|
|
903
1031
|
|
|
904
1032
|
/**
|
|
905
1033
|
* Initialise le Scroll Worker avec les données actuelles
|
|
@@ -2311,23 +2439,29 @@ class CanvasFramework {
|
|
|
2311
2439
|
}
|
|
2312
2440
|
|
|
2313
2441
|
handleTouchEnd(e) {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2442
|
+
e.preventDefault();
|
|
2443
|
+
const touch = e.changedTouches[0];
|
|
2444
|
+
const pos = this.getTouchPos(touch);
|
|
2317
2445
|
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2446
|
+
if (!this.isDragging) {
|
|
2447
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'end');
|
|
2448
|
+
} else {
|
|
2449
|
+
this.isDragging = false;
|
|
2450
|
+
this.scrollWorker.postMessage({
|
|
2451
|
+
type: 'SET_DRAGGING',
|
|
2452
|
+
payload: {
|
|
2453
|
+
isDragging: false,
|
|
2454
|
+
lastVelocity: this.scrollVelocity
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
2457
|
+
|
|
2458
|
+
// ✅ AJOUTER: Démarrer l'animation de retour
|
|
2459
|
+
if (!this.isOverscrollAnimating) {
|
|
2460
|
+
this.isOverscrollAnimating = true;
|
|
2461
|
+
this.scrollWorker.postMessage({ type: 'ANIMATE_RETURN' });
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2331
2465
|
|
|
2332
2466
|
handleMouseDown(e) {
|
|
2333
2467
|
this.isDragging = false;
|
|
@@ -2375,21 +2509,26 @@ class CanvasFramework {
|
|
|
2375
2509
|
}
|
|
2376
2510
|
}
|
|
2377
2511
|
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2512
|
+
handleMouseUp(e) {
|
|
2513
|
+
if (!this.isDragging) {
|
|
2514
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
|
|
2515
|
+
} else {
|
|
2516
|
+
this.isDragging = false;
|
|
2517
|
+
this.scrollWorker.postMessage({
|
|
2518
|
+
type: 'SET_DRAGGING',
|
|
2519
|
+
payload: {
|
|
2520
|
+
isDragging: false,
|
|
2521
|
+
lastVelocity: this.scrollVelocity
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
|
|
2525
|
+
// ✅ AJOUTER: Démarrer l'animation de retour
|
|
2526
|
+
if (!this.isOverscrollAnimating) {
|
|
2527
|
+
this.isOverscrollAnimating = true;
|
|
2528
|
+
this.scrollWorker.postMessage({ type: 'ANIMATE_RETURN' });
|
|
2390
2529
|
}
|
|
2391
2530
|
}
|
|
2392
|
-
|
|
2531
|
+
}
|
|
2393
2532
|
|
|
2394
2533
|
getTouchPos(touch) {
|
|
2395
2534
|
const rect = this.canvas.getBoundingClientRect();
|
|
@@ -2988,6 +3127,46 @@ class CanvasFramework {
|
|
|
2988
3127
|
this.renderFull();
|
|
2989
3128
|
}
|
|
2990
3129
|
}
|
|
3130
|
+
|
|
3131
|
+
/**
|
|
3132
|
+
* Dessine l'effet d'overscroll (overlay gris)
|
|
3133
|
+
*/
|
|
3134
|
+
drawOverscrollEffect() {console.log('dessine');
|
|
3135
|
+
if (Math.abs(this.overscrollDistance) < 1) return;
|
|
3136
|
+
|
|
3137
|
+
const ctx = this.ctx;
|
|
3138
|
+
ctx.save();
|
|
3139
|
+
|
|
3140
|
+
// Calculer l'opacité (max 0.4 pour Android)
|
|
3141
|
+
const maxOverscroll = 150;
|
|
3142
|
+
const opacity = Math.min(Math.abs(this.overscrollDistance) / maxOverscroll, 1) * 0.4;
|
|
3143
|
+
|
|
3144
|
+
// Hauteur de l'overlay
|
|
3145
|
+
const overlayHeight = Math.min(Math.abs(this.overscrollDistance) * 1.2, 250);
|
|
3146
|
+
|
|
3147
|
+
let gradient;
|
|
3148
|
+
|
|
3149
|
+
// Overscroll en haut
|
|
3150
|
+
if (this.overscrollDistance > 0) {
|
|
3151
|
+
gradient = ctx.createLinearGradient(0, 0, 0, overlayHeight);
|
|
3152
|
+
gradient.addColorStop(0, `rgba(100, 100, 100, ${opacity})`);
|
|
3153
|
+
gradient.addColorStop(1, 'rgba(100, 100, 100, 0)');
|
|
3154
|
+
|
|
3155
|
+
ctx.fillStyle = gradient;
|
|
3156
|
+
ctx.fillRect(0, 0, this.width, overlayHeight);
|
|
3157
|
+
}
|
|
3158
|
+
// Overscroll en bas
|
|
3159
|
+
else if (this.overscrollDistance < 0) {
|
|
3160
|
+
gradient = ctx.createLinearGradient(0, this.height - overlayHeight, 0, this.height);
|
|
3161
|
+
gradient.addColorStop(0, 'rgba(100, 100, 100, 0)');
|
|
3162
|
+
gradient.addColorStop(1, `rgba(100, 100, 100, ${opacity})`);
|
|
3163
|
+
|
|
3164
|
+
ctx.fillStyle = gradient;
|
|
3165
|
+
ctx.fillRect(0, this.height - overlayHeight, this.width, overlayHeight);
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
ctx.restore();
|
|
3169
|
+
}
|
|
2991
3170
|
|
|
2992
3171
|
/**
|
|
2993
3172
|
* Rendu normal (sans transition)
|