canvasframework 0.5.9 → 0.5.11
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 +1 -1
- package/components/Camera.js +20 -0
- package/components/FloatedCamera.js +21 -1
- package/core/CanvasFramework.js +217 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Canvas UI Engine (UI engine inspired by Flutter, built for the Web)
|
|
5
5
|
|
|
6
6
|
> **Canvas-based UI Engine for Mobile & Embedded Apps**
|
|
7
|
-
> A high-performance UI engine rendered with Canvas/WebGL
|
|
7
|
+
> A high-performance UI engine rendered with Canvas/WebGL, running inside a WebView runtime (Capacitor / Cordova), without DOM, HTML or CSS.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
package/components/Camera.js
CHANGED
|
@@ -34,6 +34,25 @@ class Camera extends Component {
|
|
|
34
34
|
|
|
35
35
|
this.isStarting = false;
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
static cleanupAllCameras() {
|
|
39
|
+
const allVideos = document.querySelectorAll('video');
|
|
40
|
+
allVideos.forEach(video => {
|
|
41
|
+
// Arrêter les streams
|
|
42
|
+
if (video.srcObject) {
|
|
43
|
+
const stream = video.srcObject;
|
|
44
|
+
if (stream && stream.getTracks) {
|
|
45
|
+
stream.getTracks().forEach(track => track.stop());
|
|
46
|
+
}
|
|
47
|
+
video.srcObject = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Supprimer du DOM
|
|
51
|
+
if (video.parentNode) {
|
|
52
|
+
video.parentNode.removeChild(video);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
37
56
|
|
|
38
57
|
async _mount() {
|
|
39
58
|
super._mount?.();
|
|
@@ -120,6 +139,7 @@ class Camera extends Component {
|
|
|
120
139
|
handleMouseUp(e) {}
|
|
121
140
|
|
|
122
141
|
async startCamera() {
|
|
142
|
+
Camera.cleanupAllCameras();
|
|
123
143
|
if (this.stream) return;
|
|
124
144
|
|
|
125
145
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Composant
|
|
4
|
+
* Composant Camera autonome avec gestion directe des clics/touches
|
|
5
5
|
* Modes : contain (tout visible + bandes), cover (remplit + crop), fit (centre sans crop)
|
|
6
6
|
*/
|
|
7
7
|
class FloatedCamera extends Component {
|
|
@@ -34,6 +34,25 @@ class FloatedCamera extends Component {
|
|
|
34
34
|
|
|
35
35
|
this.isStarting = false;
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
static cleanupAllCameras() {
|
|
39
|
+
const allVideos = document.querySelectorAll('video');
|
|
40
|
+
allVideos.forEach(video => {
|
|
41
|
+
// Arrêter les streams
|
|
42
|
+
if (video.srcObject) {
|
|
43
|
+
const stream = video.srcObject;
|
|
44
|
+
if (stream && stream.getTracks) {
|
|
45
|
+
stream.getTracks().forEach(track => track.stop());
|
|
46
|
+
}
|
|
47
|
+
video.srcObject = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Supprimer du DOM
|
|
51
|
+
if (video.parentNode) {
|
|
52
|
+
video.parentNode.removeChild(video);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
37
56
|
|
|
38
57
|
async _mount() {
|
|
39
58
|
super._mount?.();
|
|
@@ -120,6 +139,7 @@ class FloatedCamera extends Component {
|
|
|
120
139
|
handleMouseUp(e) {}
|
|
121
140
|
|
|
122
141
|
async startCamera() {
|
|
142
|
+
FloatedCamera.cleanupAllCameras();
|
|
123
143
|
if (this.stream) return;
|
|
124
144
|
|
|
125
145
|
try {
|
package/core/CanvasFramework.js
CHANGED
|
@@ -1781,6 +1781,7 @@ class CanvasFramework {
|
|
|
1781
1781
|
* @private
|
|
1782
1782
|
*/
|
|
1783
1783
|
async navigateTo(path, options = {}) {
|
|
1784
|
+
|
|
1784
1785
|
const {
|
|
1785
1786
|
replace = false,
|
|
1786
1787
|
animate = true,
|
|
@@ -1803,7 +1804,7 @@ class CanvasFramework {
|
|
|
1803
1804
|
} = match;
|
|
1804
1805
|
|
|
1805
1806
|
// ===== LIFECYCLE: AVANT DE QUITTER L'ANCIENNE ROUTE =====
|
|
1806
|
-
|
|
1807
|
+
|
|
1807
1808
|
// Hook beforeLeave de la route actuelle (peut bloquer la navigation)
|
|
1808
1809
|
const currentRouteData = this.routes.get(this.currentRoute);
|
|
1809
1810
|
if (currentRouteData?.beforeLeave) {
|
|
@@ -1903,9 +1904,10 @@ class CanvasFramework {
|
|
|
1903
1904
|
// ===== LANCER L'ANIMATION DE TRANSITION =====
|
|
1904
1905
|
|
|
1905
1906
|
if (animate && !this.transitionState.isTransitioning) {
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1907
|
+
const transitionType = transition || route.transition || 'slide';
|
|
1908
|
+
this.startTransition(oldComponents, this.components, transitionType, direction);
|
|
1909
|
+
// Pas besoin d'appeler renderFull() ici, la boucle de rendu s'en chargera
|
|
1910
|
+
}
|
|
1909
1911
|
|
|
1910
1912
|
// ===== LIFECYCLE: APRÈS ÊTRE ENTRÉ DANS LA NOUVELLE ROUTE =====
|
|
1911
1913
|
|
|
@@ -1929,6 +1931,17 @@ class CanvasFramework {
|
|
|
1929
1931
|
|
|
1930
1932
|
// ✅ OPTIONNEL : Marquer les composants comme "dirty" pour forcer le rendu
|
|
1931
1933
|
this._maxScrollDirty = true;
|
|
1934
|
+
|
|
1935
|
+
this.components.forEach(comp => {
|
|
1936
|
+
// Vérifier si c'est un composant caméra
|
|
1937
|
+
if (comp.constructor.name === 'FloatedCamera' || comp.constructor.name === 'Camera') {
|
|
1938
|
+
// Arrêter le stream vidéo
|
|
1939
|
+
if (comp.stopCamera && typeof comp.stopCamera === 'function') {
|
|
1940
|
+
comp.stopCamera();
|
|
1941
|
+
console.log(`🎥 Caméra ${comp.constructor.name} arrêtée avant navigation`);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1932
1945
|
}
|
|
1933
1946
|
|
|
1934
1947
|
/**
|
|
@@ -2661,7 +2674,6 @@ class CanvasFramework {
|
|
|
2661
2674
|
|
|
2662
2675
|
startRenderLoop() {
|
|
2663
2676
|
let lastScrollOffset = this.scrollOffset;
|
|
2664
|
-
let lastRenderMode = 'full';
|
|
2665
2677
|
|
|
2666
2678
|
const render = () => {
|
|
2667
2679
|
if (!this._splashFinished) {
|
|
@@ -2669,33 +2681,31 @@ class CanvasFramework {
|
|
|
2669
2681
|
return;
|
|
2670
2682
|
}
|
|
2671
2683
|
|
|
2672
|
-
//
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
// Décider du mode de rendu
|
|
2677
|
-
let renderMode = 'full';
|
|
2678
|
-
|
|
2679
|
-
if (this.optimizationEnabled &&
|
|
2680
|
-
this.dirtyComponents.size > 0 &&
|
|
2681
|
-
!this.isDragging &&
|
|
2682
|
-
Math.abs(this.scrollVelocity) < 0.5 &&
|
|
2683
|
-
!scrollChanged &&
|
|
2684
|
-
this.dirtyComponents.size < 3) {
|
|
2685
|
-
renderMode = 'dirty';
|
|
2686
|
-
}
|
|
2684
|
+
// TOUJOURS effacer l'écran en premier
|
|
2685
|
+
this.ctx.fillStyle = this.backgroundColor || '#ffffff';
|
|
2686
|
+
this.ctx.fillRect(0, 0, this.width, this.height);
|
|
2687
2687
|
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2688
|
+
// Si une transition est en cours
|
|
2689
|
+
if (this.transitionState.isTransitioning) {
|
|
2690
|
+
// Mettre à jour la progression
|
|
2691
|
+
const elapsed = Date.now() - this.transitionState.startTime;
|
|
2692
|
+
this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
|
|
2693
|
+
|
|
2694
|
+
// Rendu spécial pour la transition
|
|
2695
|
+
this.renderSimpleTransition();
|
|
2696
|
+
|
|
2697
|
+
// Si la transition est terminée
|
|
2698
|
+
if (this.transitionState.progress >= 1) {
|
|
2699
|
+
this.transitionState.isTransitioning = false;
|
|
2700
|
+
this.transitionState.oldComponents = [];
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
// Sinon, rendu normal
|
|
2704
|
+
else {
|
|
2691
2705
|
this.renderFull();
|
|
2692
|
-
} else {
|
|
2693
|
-
this._renderDirtyComponents();
|
|
2694
2706
|
}
|
|
2695
2707
|
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
// ✅ AJOUTER : Calcul et affichage du FPS
|
|
2708
|
+
// Calcul FPS (optionnel)
|
|
2699
2709
|
this._frames++;
|
|
2700
2710
|
const now = performance.now();
|
|
2701
2711
|
const elapsed = now - this._lastFpsTime;
|
|
@@ -2706,7 +2716,6 @@ class CanvasFramework {
|
|
|
2706
2716
|
this._lastFpsTime = now;
|
|
2707
2717
|
}
|
|
2708
2718
|
|
|
2709
|
-
// ✅ AJOUTER : Afficher le FPS si activé
|
|
2710
2719
|
if (this.showFps) {
|
|
2711
2720
|
this.ctx.save();
|
|
2712
2721
|
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
@@ -2719,17 +2728,7 @@ class CanvasFramework {
|
|
|
2719
2728
|
this.ctx.restore();
|
|
2720
2729
|
}
|
|
2721
2730
|
|
|
2722
|
-
//
|
|
2723
|
-
if (this.debbug) {
|
|
2724
|
-
this.drawOverflowIndicators();
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
// ✅ AJOUTER : Marquer le premier rendu
|
|
2728
|
-
if (!this._firstRenderDone) {
|
|
2729
|
-
this._markFirstRender();
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
// ✅ AJOUTER : Mettre à jour l'inertie si nécessaire
|
|
2731
|
+
// Mettre à jour l'inertie du scroll
|
|
2733
2732
|
if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
|
|
2734
2733
|
this.scrollWorker.postMessage({ type: 'UPDATE_INERTIA' });
|
|
2735
2734
|
}
|
|
@@ -2740,53 +2739,186 @@ class CanvasFramework {
|
|
|
2740
2739
|
render();
|
|
2741
2740
|
}
|
|
2742
2741
|
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2742
|
+
/**
|
|
2743
|
+
* Rendu ultra simple pour la transition slide
|
|
2744
|
+
*/
|
|
2745
|
+
renderSimpleTransition() {
|
|
2746
|
+
const { progress, type, direction, oldComponents, newComponents } = this.transitionState;
|
|
2747
|
+
|
|
2748
|
+
// Calculer la position de décalage
|
|
2749
|
+
const slideOffset = this.width * (1 - progress);
|
|
2750
|
+
const directionMultiplier = direction === 'forward' ? 1 : -1;
|
|
2751
|
+
|
|
2752
|
+
// Pour l'animation slide, dessiner seulement la nouvelle vue à la bonne position
|
|
2753
|
+
if (type === 'slide') {
|
|
2754
|
+
// Sauvegarder le contexte
|
|
2755
|
+
this.ctx.save();
|
|
2756
|
+
|
|
2757
|
+
// Appliquer la translation pour l'animation
|
|
2758
|
+
this.ctx.translate(slideOffset * directionMultiplier, 0);
|
|
2759
|
+
|
|
2760
|
+
// Dessiner UNIQUEMENT les nouveaux composants
|
|
2761
|
+
for (let comp of newComponents) {
|
|
2762
|
+
if (comp && comp.visible) {
|
|
2763
|
+
const isFixed = this.isFixedComponent(comp);
|
|
2764
|
+
|
|
2765
|
+
if (isFixed) {
|
|
2766
|
+
comp.draw(this.ctx);
|
|
2767
|
+
} else {
|
|
2768
|
+
// Pour les composants scrollables, ajouter le décalage
|
|
2769
|
+
this.ctx.save();
|
|
2770
|
+
this.ctx.translate(0, this.scrollOffset);
|
|
2771
|
+
comp.draw(this.ctx);
|
|
2772
|
+
this.ctx.restore();
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
this.ctx.restore();
|
|
2778
|
+
}
|
|
2779
|
+
// Pour fade, dessiner la nouvelle vue avec alpha
|
|
2780
|
+
else if (type === 'fade') {
|
|
2781
|
+
this.ctx.save();
|
|
2782
|
+
this.ctx.globalAlpha = progress;
|
|
2783
|
+
|
|
2784
|
+
for (let comp of newComponents) {
|
|
2785
|
+
if (comp && comp.visible) {
|
|
2786
|
+
const isFixed = this.isFixedComponent(comp);
|
|
2787
|
+
|
|
2788
|
+
if (isFixed) {
|
|
2789
|
+
comp.draw(this.ctx);
|
|
2790
|
+
} else {
|
|
2791
|
+
this.ctx.save();
|
|
2792
|
+
this.ctx.translate(0, this.scrollOffset);
|
|
2793
|
+
comp.draw(this.ctx);
|
|
2794
|
+
this.ctx.restore();
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
this.ctx.restore();
|
|
2800
|
+
}
|
|
2801
|
+
// Pour 'none', dessiner normalement
|
|
2802
|
+
else {
|
|
2803
|
+
this.renderFull();
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2776
2806
|
|
|
2777
|
-
|
|
2778
|
-
|
|
2807
|
+
/**
|
|
2808
|
+
* Rendu normal (sans transition)
|
|
2809
|
+
*/
|
|
2810
|
+
renderFull() {
|
|
2811
|
+
this.ctx.save();
|
|
2812
|
+
|
|
2813
|
+
// Séparer les composants fixes et scrollables
|
|
2814
|
+
const scrollableComponents = [];
|
|
2815
|
+
const fixedComponents = [];
|
|
2816
|
+
|
|
2817
|
+
for (let comp of this.components) {
|
|
2818
|
+
if (this.isFixedComponent(comp)) {
|
|
2819
|
+
fixedComponents.push(comp);
|
|
2820
|
+
} else {
|
|
2821
|
+
scrollableComponents.push(comp);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// Dessiner les composants scrollables
|
|
2826
|
+
if (scrollableComponents.length > 0) {
|
|
2827
|
+
this.ctx.save();
|
|
2828
|
+
this.ctx.translate(0, this.scrollOffset);
|
|
2829
|
+
|
|
2830
|
+
for (let comp of scrollableComponents) {
|
|
2831
|
+
if (comp.visible) {
|
|
2832
|
+
comp.draw(this.ctx);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
this.ctx.restore();
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// Dessiner les composants fixes
|
|
2840
|
+
for (let comp of fixedComponents) {
|
|
2841
|
+
if (comp.visible) {
|
|
2842
|
+
comp.draw(this.ctx);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
this.ctx.restore();
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
/**
|
|
2850
|
+
* Mettre à jour la progression de la transition
|
|
2851
|
+
* @private
|
|
2852
|
+
*/
|
|
2853
|
+
updateTransition() {
|
|
2854
|
+
if (!this.transitionState.isTransitioning) return;
|
|
2855
|
+
|
|
2856
|
+
const elapsed = Date.now() - this.transitionState.startTime;
|
|
2857
|
+
this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
|
|
2858
|
+
|
|
2859
|
+
// Si la transition est terminée
|
|
2860
|
+
if (this.transitionState.progress >= 1) {
|
|
2861
|
+
this.transitionState.isTransitioning = false;
|
|
2862
|
+
|
|
2863
|
+
// Marquer tous les nouveaux composants comme sales pour le prochain rendu
|
|
2864
|
+
this.transitionState.newComponents.forEach(comp => {
|
|
2865
|
+
this.markComponentDirty(comp);
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2779
2869
|
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2870
|
+
/**
|
|
2871
|
+
* Rendu complet normal (sans transition)
|
|
2872
|
+
* @private
|
|
2873
|
+
*/
|
|
2874
|
+
renderFull() {
|
|
2875
|
+
// Sauvegarder le contexte
|
|
2876
|
+
this.ctx.save();
|
|
2877
|
+
|
|
2878
|
+
// Séparer les composants
|
|
2879
|
+
const scrollableComponents = [];
|
|
2880
|
+
const fixedComponents = [];
|
|
2881
|
+
|
|
2882
|
+
for (let comp of this.components) {
|
|
2883
|
+
if (this.isFixedComponent(comp)) {
|
|
2884
|
+
fixedComponents.push(comp);
|
|
2885
|
+
} else {
|
|
2886
|
+
scrollableComponents.push(comp);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// Dessiner les composants scrollables avec translation
|
|
2891
|
+
if (scrollableComponents.length > 0) {
|
|
2892
|
+
this.ctx.save();
|
|
2893
|
+
this.ctx.translate(0, this.scrollOffset);
|
|
2894
|
+
|
|
2895
|
+
for (let comp of scrollableComponents) {
|
|
2896
|
+
if (comp.visible) {
|
|
2897
|
+
// Viewport culling
|
|
2898
|
+
const screenY = comp.y + this.scrollOffset;
|
|
2899
|
+
const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
|
|
2900
|
+
|
|
2901
|
+
if (isInViewport) {
|
|
2902
|
+
comp.draw(this.ctx);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
this.ctx.restore();
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// Dessiner les composants fixes
|
|
2911
|
+
for (let comp of fixedComponents) {
|
|
2912
|
+
if (comp.visible) {
|
|
2913
|
+
comp.draw(this.ctx);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// Restaurer le contexte
|
|
2918
|
+
this.ctx.restore();
|
|
2919
|
+
}
|
|
2786
2920
|
|
|
2787
|
-
|
|
2788
|
-
this.ctx.restore();
|
|
2789
|
-
}
|
|
2921
|
+
|
|
2790
2922
|
|
|
2791
2923
|
/**
|
|
2792
2924
|
* Fait défiler à une position spécifique
|
|
@@ -2901,8 +3033,4 @@ class CanvasFramework {
|
|
|
2901
3033
|
}
|
|
2902
3034
|
}
|
|
2903
3035
|
|
|
2904
|
-
export default CanvasFramework;
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
3036
|
+
export default CanvasFramework;
|