reactjsnode.jsthree.js

REACT charge a lot of images


Hello so i'm doing an webpage i want to load very high quality images so for te moment the webpage serve the images in like 10s i did a charging srceen for it but it's to long so i made 3 dir for low mid and high but my charging screen stops just when the high quality is loaded so it didn't changed enything. I also have a node.js serveur but i think i need to do it by my frontend.

import './contact.css'
import './pics.css'
import { MyHeader } from './Welcome'
import { useGLTF, useProgress } from '@react-three/drei'
import React, { useRef, useState, useEffect, Suspense } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
function MyModel() {
  const gltf = useGLTF('/3dModels/pictures_shower.glb')
  return <primitive object={gltf.scene} position={[0, -1.5, 0]} scale={2.5} />
}

export default function Pics() {
  const [model, setModel] = useState('none')
  return (
    <React.StrictMode>
      <MyHeader setModel={setModel} />

      <VitrineCanvas />
    </React.StrictMode>
  )
}

function VitrineCanvas() {
  const qualityLevels = ['low', 'mid', 'high']
  const imageThemes = ['noir-blanc', 'nerd', 'bg']
  const imagesNum = [8, 18, 31] // nombre d’images dans chaque dossier respectif
  const [image, setImage] = useState(0)
  const [isLoading, setIsLoading] = useState(true)
  const [loadedCount, setLoadedCount] = useState(0)
  const imagesDirs = ['noir-blanc', 'nerd', 'bg']
  const preloadDone = useRef(false)
  const totalCount = imagesNum.reduce((a, b) => a + b, 0)
  const [showCanvas, setShowCanvas] = useState(false)
  const { active, loaded, total, progress } = useProgress()
  const [canvasReady, setCanvasReady] = useState(false)
  const [gltfLoaded, setGLTFLoaded] = useState(false)
  const [imagesLoaded, setImagesLoaded] = useState(false)
  const [rotationOffset, setRotationOffset] = useState(0)
  const [imagesOppen, setImagesOppen] = useState(false)
  useEffect(() => {
    const container = document.querySelector('.carousel-3d')
    if (!container) return

    const onWheel = (e) => {
      e.preventDefault()
      const delta = e.deltaY > 0 ? 1 : -1
      setRotationOffset((prev) => prev + delta * (360 / imagesNum[image])) // 20° par scroll
    }

    container.addEventListener('wheel', onWheel)
    return () => container.removeEventListener('wheel', onWheel)
  }, [image])
  useEffect(() => {
    if (imagesLoaded && gltfLoaded) {
      setIsLoading(false)
      setTimeout(() => {
        setShowCanvas(true)
      }, 150)
    }
  }, [imagesLoaded, gltfLoaded])
  useEffect(() => {
    const loader = new GLTFLoader()
    loader.load(
      '/3dModels/pictures_shower.glb',
      (gltf) => {
        console.log('✅ GLTF préchargé')
        setGLTFLoaded(true)
      },
      undefined,
      (error) => {
        console.error('❌ Erreur lors du chargement GLTF', error)
      }
    )
  }, [])
  useEffect(() => {
    if (!isLoading && showCanvas && canvasReady) {
      const themeCount = imageThemes.length

      const swapThemeImages = (themeIndex = 0) => {
        if (themeIndex >= themeCount) return

        let i = 1
        const swapNext = () => {
          if (i > imagesNum[themeIndex]) {
            setTimeout(() => swapThemeImages(themeIndex + 1), 200) // passe au dossier suivant
            return
          }

          const imgClass = `.VitrineImg-${themeIndex}-${i}`
          const imgElement = document.querySelector(imgClass)
          if (imgElement) {
            const midSrc = `/pictures/mid/${imageThemes[themeIndex]}/${i}.jpg`
            const newImg = new Image()
            newImg.onload = () => {
              imgElement.src = midSrc
              i++
              setTimeout(swapNext, 50)
            }
            newImg.onerror = () => {
              i++
              setTimeout(swapNext, 50)
            }
            newImg.src = midSrc
          } else {
            i++
            setTimeout(swapNext, 50)
          }
        }

        swapNext()
      }

      setTimeout(() => {
        swapThemeImages()
      }, 1000) // laisse tout se stabiliser d’abord
    }
  }, [isLoading, showCanvas, canvasReady])
  useEffect(() => {
    if (!active && loaded === total && progress === 100) {
      setCanvasReady(true)
    }
  }, [active, loaded, total, progress])
  useEffect(() => {
    if (preloadDone.current) return

    let total = 0
    const preload = (dir, count, callback) => {
      let i = 1
      const loadNext = () => {
        if (i > count) {
          callback()
          return
        }

        const img = new Image()
        const onLoadOrError = () => {
          total++
          setLoadedCount(total)
          i++
          setTimeout(loadNext, 150) // délai entre les images
        }

        img.onload = onLoadOrError
        img.onerror = onLoadOrError
        img.src = `/pictures/${dir}/${i}.jpg`
      }
      loadNext()
    }

    let done = 0
    const onPartDone = () => {
      done++
      if (done === imagesDirs.length) {
        setImagesLoaded(true)
        if (gltfLoaded) {
          setIsLoading(false)
          setTimeout(() => {
            setShowCanvas(true)
          }, 150)
        }
      }
    }

    for (let d = 0; d < imagesDirs.length; d++) {
      preload(imagesDirs[d], imagesNum[d], onPartDone)
    }
  }, [])
  useEffect(() => {
    const allImages = document.querySelectorAll('.carousel-3d img')
    const visibleImages = Array.from(allImages).filter((img) => {
      const rect = img.getBoundingClientRect()
      return (
        rect.width > 0 &&
        rect.height > 0 &&
        window.getComputedStyle(img).display !== 'none' &&
        img.offsetParent !== null
      )
    })

    const total = visibleImages.length
    const anglePerImage = 360 / total
    const centerAngle = 0 // l’angle de la caméra en face
    const radius = 200
    let closestImage = null
    let closestDelta = Infinity

    // 1. Déterminer quelle image est la plus proche du centre
    visibleImages.forEach((img, i) => {
      const angle = (anglePerImage * i + rotationOffset) % 360
      const delta = Math.abs(((angle - centerAngle + 540) % 360) - 180)
      if (delta < closestDelta) {
        closestDelta = delta
        closestImage = img
      }
    })

    // 2. Appliquer les styles à chaque image
    visibleImages.forEach((img, i) => {
      const angle = (anglePerImage * i + rotationOffset) % 360
      const isCentered = img === closestImage
      const scale = isCentered ? 2 : 1.2
      const z = isCentered ? radius + 50 : radius
      const zIndex = isCentered ? 100 : 0

      img.style.transform = `
      rotateY(${angle}deg)
      translateZ(${z}px)
      scale(${scale})
    `
      img.style.zIndex = String(zIndex)
    })
  }, [image, rotationOffset])
  const allImages = []
  for (let themeIndex = 0; themeIndex < imageThemes.length; themeIndex++) {
    for (let i = 1; i <= imagesNum[themeIndex]; i++) {
      allImages.push(
        <img
          key={`img-${themeIndex}-${i}`}
          src={`/pictures/low/${imageThemes[themeIndex]}/${i}.jpg`}
          className={`VitrineImg-${themeIndex}-${i} VistrineImgs`}
          style={{ display: image === themeIndex ? 'block' : 'none' }}
        />
      )
    }
  }
  console.log(imagesOppen)
  return (
    <>
      <div
        className="loader"
        style={
          isLoading
            ? { zIndex: '9999', animationName: 'LoaderArrive', opacity: 1 }
            : { zIndex: '-9999', animationName: 'loaderDisapear', opacity: 0 }
        }
      >
        Chargement des images… {loadedCount} / {totalCount}
      </div>
      {
        <Canvas
          style={{
            width: '100vw',
            height: 'calc(100vh - 100px)',
            marginTop: '0',
          }}
          onClick={() => {
            const imagesRot = [0, 6.28311 / 3 + 0.2, 6.28311 / 1.5]
            rotation = imagesRot[image]
            setImagesOppen(!imagesOppen)
            console.log('hello world')
          }}
          camera={{ position: [0, 5, 0], fov: 50 }}
        >
          <ambientLight intensity={0.25} />
          <pointLight position={[0, 5, 5]} intensity={0.25} />
          <pointLight position={[5, 5, 10]} intensity={0.25} />
          <pointLight position={[5, 5, 5]} intensity={0.25} />
          <pointLight position={[-5, 5, -5]} intensity={0.25} />
          <Vitrine
            position={[-1, 0, 0]}
            image={image}
            setImage={setImage}
            imagesOppen={imagesOppen}
          />
        </Canvas>
      }
      <div
        className="VitrineImages"
        style={imagesOppen ? { opacity: '1' } : { opacity: '0' }}
      >
        <div className="carousel-3d">{allImages}</div>
      </div>
    </>
  )
}

var rotation = 0
var tBefor = 0
function Vitrine({ image, setImage, imagesOppen }) {
  const cubeRef = useRef()

  useFrame(({ clock }) => {
    cubeRef.current.position.y = 1
    cubeRef.current.position.z = 0.5
    const t = clock.getElapsedTime()
    var tour = Math.floor((cubeRef.current.rotation.y + 1) / 6.283118)
    var beforImage = image
    const newImage = Math.floor(
      (cubeRef.current.rotation.y + 1 - tour * 6.283118) / 6.283119 / 0.33
    )

    if (newImage !== image) {
      setImage(newImage)
    }
    cubeRef.current.scale.set(0.45, 0.45, 0.45)
    cubeRef.current.rotation.x = (3.14 / 2) * 3
    var arriveOnImage = true
    if (!arriveOnImage) {
      arriveOnImage = boforImage != image ? true : false
    }
    if (
      tour * 6.283118 + (image - 1) * (3.14 / 1.5) + 2.5 <=
      cubeRef.current.rotation.y + 1
    ) {
      arriveOnImage = false
    }

    if (cubeRef.current) {
      if (!imagesOppen) {
        if (arriveOnImage) {
          rotation += 1 * (t - tBefor)
        } else {
          rotation += 0.75 * (t - tBefor)
        }
      }
      cubeRef.current.rotation.y = rotation

      // légère rotation Y
    }
    tBefor = t
  })

  return (
    <mesh ref={cubeRef}>
      <MyModel />
    </mesh>
  )
}

Solution

  • How to implement:

    1. Modify VitrineCanvas to use IntersectionObserver:

      • For each image in your allImages array, initially set its src to the low-quality version.

      • Attach an IntersectionObserver to each image element.

      • When an image enters the viewport (or a certain threshold near it), update its src to the mid-quality version.

      • You could even have another step to load the high-quality version when it's more centrally focused in the carousel.

    2. Example IntersectionObserver logic (Conceptual):

      JavaScript

      // Inside VitrineCanvas component
      useEffect(() => {
          const imageElements = document.querySelectorAll('.VitrineImages img');
          const observerOptions = {
              root: null, // viewport
              rootMargin: '0px',
              threshold: 0.1 // Trigger when 10% of the image is visible
          };
      
          const imageObserver = new IntersectionObserver((entries, observer) => {
              entries.forEach(entry => {
                  if (entry.isIntersecting) {
                      const img = entry.target;
                      const fullSrc = img.dataset.fullSrc; // Store mid/high quality URL in a data attribute
                      if (img.src !== fullSrc) { // Prevent re-loading if already loaded
                          const tempImg = new Image();
                          tempImg.src = fullSrc;
                          tempImg.onload = () => {
                              img.src = fullSrc;
                              img.classList.add('loaded'); // Add a class for styling if needed
                          };
                          observer.unobserve(img); // Stop observing once loaded
                      }
                  }
              });
          }, observerOptions);
      
          imageElements.forEach(img => {
              // Set a data attribute with the mid-quality source
              const themeIndex = parseInt(img.dataset.themeIndex);
              const imgNumber = parseInt(img.dataset.imgNumber);
              img.dataset.fullSrc = `/pictures/mid/<span class="math-inline">\{imageThemes\[themeIndex\]\}/</span>{imgNumber}.jpg`; // Or high
              observer.observe(img);
          });
      
          return () => {
              imageElements.forEach(img => imageObserver.unobserve(img));
          };
      }, [imageThemes, imagesNum]); // Dependencies for useEffect
      
      

      You'd need to adjust your allImages.push() to include data-full-src and other data attributes like data-theme-index and data-img-number for easier reference.


    2. Leverage Your Node.js Server for Image Optimization

    While you stated you think you need to do it by your frontend, your Node.js server is actually a powerful tool for solving this problem.

    What to do:

    How to implement:

    1. Server-Side Image Processing Library:

      • Use libraries like Sharp or Jimp in Node.js to resize and compress images.

      • You can set up an API endpoint like /images/:quality/:theme/:id.jpg.

      • When a request comes in, your Node.js server can:

        • Check if the requested quality (e.g., low, mid, high) exists.

        • If not, process the original high-quality image to the requested size/quality, save it (to avoid re-processing), and then send it.

      • This is called on-demand image optimization.

    2. Example Server-Side Logic (Conceptual with Express and Sharp):

      JavaScript

      // In your Node.js server file
      const express = require('express');
      const sharp = require('sharp');
      const path = require('path');
      const fs = require('fs');
      
      const app = express();
      const imagesBaseDir = path.join(__dirname, 'public', 'pictures'); // Adjust path
      
      app.get('/pictures/:quality/:theme/:filename', async (req, res) => {
          const { quality, theme, filename } = req.params;
          const originalImagePath = path.join(imagesBaseDir, 'original', theme, filename); // Assume 'original' folder for highest quality
      
          if (!fs.existsSync(originalImagePath)) {
              return res.status(404).send('Image not found');
          }
      
          try {
              let imageStream = sharp(originalImagePath);
      
              switch (quality) {
                  case 'low':
                      imageStream = imageStream.resize({ width: 300 }).jpeg({ quality: 50 });
                      break;
                  case 'mid':
                      imageStream = imageStream.resize({ width: 800 }).jpeg({ quality: 75 });
                      break;
                  case 'high':
                      imageStream = imageStream.resize({ width: 1600 }).jpeg({ quality: 90 });
                      break;
                  default:
                      imageStream = imageStream.jpeg({ quality: 80 }); // Default to a good quality
              }
      
              res.setHeader('Content-Type', 'image/jpeg'); // Or image/png, etc.
              imageStream.pipe(res);
      
          } catch (error) {
              console.error('Error processing image:', error);
              res.status(500).send('Error processing image');
          }
      });
      
      // ... other server setup
      
      

      You would then update your frontend src attributes to point to these new server endpoints.


    3. Optimize 3D Model Loading

    While you're preloading the GLTF model, ensure it's also optimized.

    What to do:

    How to implement:

    Bash

    # Install gltf-pipeline
    npm install -g gltf-pipeline
    
    # Compress your model with Draco
    gltf-pipeline -i pictures_shower.glb -o pictures_shower_draco.glb --draco
    

    Then, update your useGLTF path to the compressed version.


    4. Refine Your Loading Screen Logic

    Your current loading screen reports loadedCount / totalCount based on low-quality image preloading. You want this to reflect the actual readiness for interaction.

    What to do:

    How to implement:


    5. Consider Lazy Loading for Off-Screen Elements

    You're already doing display: none for non-selected themes, which is good. Ensure that images for inactive themes or those far off-screen in the carousel are not fetched until needed. The IntersectionObserver helps with this for images within the active carousel.


    Revised VitrineCanvas Structure (High-Level)

    JavaScript

    import './contact.css';
    import './pics.css';
    import { MyHeader } from './Welcome';
    import { useGLTF, useProgress } from '@react-three/drei';
    import React, { useRef, useState, useEffect, Suspense, useCallback } from 'react';
    import { Canvas, useFrame } from '@react-three/fiber';
    import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
    
    // ... MyModel component (unchanged)
    
    export default function Pics() {
      const [model, setModel] = useState('none');
      return (
        <React.StrictMode>
          <MyHeader setModel={setModel} />
          <VitrineCanvas />
        </React.StrictMode>
      );
    }
    
    function VitrineCanvas() {
      const qualityLevels = ['low', 'mid', 'high']; // Use these for server requests
      const imageThemes = ['noir-blanc', 'nerd', 'bg'];
      const imagesNum = [8, 18, 31]; // number of images in each respective folder
    
      const [image, setImage] = useState(0); // Currently active theme/image set
      const [isLoadingInitial, setIsLoadingInitial] = useState(true); // For initial low-res + GLTF
      const [loadedLowResCount, setLoadedLowResCount] = useState(0);
      const totalLowResImages = imagesNum.reduce((a, b) => a + b, 0);
    
      const [gltfLoaded, setGLTFLoaded] = useState(false);
      const [lowResImagesPreloaded, setLowResImagesPreloaded] = useState(false);
      const [showCanvas, setShowCanvas] = useState(false); // To control canvas visibility
      const [rotationOffset, setRotationOffset] = useState(0);
      const [imagesOpen, setImagesOpen] = useState(false); // Misspelled in original, corrected to 'Open'
    
      // --- GLTF Model Preload ---
      useEffect(() => {
        const loader = new GLTFLoader();
        loader.load(
          '/3dModels/pictures_shower_draco.glb', // Use compressed GLB if available
          (gltf) => {
            console.log('✅ GLTF preloaded');
            setGLTFLoaded(true);
          },
          undefined,
          (error) => {
            console.error('❌ Error loading GLTF', error);
          }
        );
      }, []);
    
      // --- Low-Resolution Image Preload (for initial loader) ---
      useEffect(() => {
        let loadedCount = 0;
        let imagesToLoad = totalLowResImages;
    
        if (imagesToLoad === 0) { // Handle case with no images
            setLowResImagesPreloaded(true);
            return;
        }
    
        const loadNextImage = (themeIndex = 0, imgNumber = 1) => {
            if (themeIndex >= imageThemes.length) {
                setLowResImagesPreloaded(true);
                return;
            }
            if (imgNumber > imagesNum[themeIndex]) {
                loadNextImage(themeIndex + 1, 1); // Move to next theme
                return;
            }
    
            const img = new Image();
            const src = `/pictures/low/${imageThemes[themeIndex]}/${imgNumber}.jpg`; // Assuming server handles this path
    
            img.onload = img.onerror = () => {
                loadedCount++;
                setLoadedLowResCount(loadedCount);
                // Throttle loading slightly to prevent freezing UI
                setTimeout(() => loadNextImage(themeIndex, imgNumber + 1), 10); // Small delay
            };
            img.src = src;
        };
    
        loadNextImage(); // Start loading
      }, []);
    
      // --- Determine when initial loading is complete ---
      useEffect(() => {
        if (gltfLoaded && lowResImagesPreloaded) {
          setIsLoadingInitial(false);
          setTimeout(() => {
            setShowCanvas(true);
          }, 150); // Small delay before showing canvas
        }
      }, [gltfLoaded, lowResImagesPreloaded]);
    
    
      // --- Carousel Wheel Event ---
      useEffect(() => {
        const container = document.querySelector('.carousel-3d');
        if (!container) return;
    
        const onWheel = (e) => {
          e.preventDefault();
          const delta = e.deltaY > 0 ? 1 : -1;
          // Adjust rotation calculation based on your carousel's angle per image
          setRotationOffset((prev) => prev + delta * (360 / imagesNum[image]));
        };
    
        container.addEventListener('wheel', onWheel);
        return () => container.removeEventListener('wheel', onWheel);
      }, [image, imagesNum]); // Added imagesNum to dependencies
    
    
      // --- Intersection Observer for Progressive Image Loading ---
      useEffect(() => {
        if (!showCanvas) return; // Only set up observer once canvas is shown
    
        const observerOptions = {
            root: null, // viewport
            rootMargin: '150px', // Load images when they are 150px away from viewport
            threshold: 0.01 // Small threshold to trigger early
        };
    
        const imageObserver = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const imgElement = entry.target;
                    const midSrc = imgElement.dataset.midSrc;
                    const highSrc = imgElement.dataset.highSrc;
    
                    // Load mid-quality if not already loaded, then high-quality
                    if (imgElement.src !== highSrc) { // If high-quality not loaded
                        const tempImg = new Image();
                        tempImg.onload = () => {
                            imgElement.src = tempImg.src; // Update src
                            // Optional: Load high quality after mid if needed
                            if (imgElement.dataset.highLoaded !== 'true' && highSrc) {
                                const highTempImg = new Image();
                                highTempImg.onload = () => {
                                    imgElement.src = highSrc;
                                    imgElement.dataset.highLoaded = 'true';
                                };
                                highTempImg.src = highSrc;
                            }
                        };
                        tempImg.onerror = () => {
                            console.warn('Failed to load image:', midSrc);
                        };
                        tempImg.src = midSrc;
                        imgElement.dataset.midLoaded = 'true'; // Mark mid as loaded
                    }
                    // Don't unobserve immediately if you want to swap between mid/high
                    // If you only want to load once and keep, then unobserve:
                    // observer.unobserve(imgElement);
                }
            });
        }, observerOptions);
    
        // Observe all images
        const allCarouselImages = document.querySelectorAll('.carousel-3d img');
        allCarouselImages.forEach(img => {
            observer.observe(img);
        });
    
        return () => {
            allCarouselImages.forEach(img => observer.unobserve(img));
        };
      }, [showCanvas, imageThemes, imagesNum]); // Re-run if these dependencies change
    
    
      // --- Carousel 3D Positioning Logic (unchanged from your original) ---
      useEffect(() => {
        const allImages = document.querySelectorAll('.carousel-3d img');
        const visibleImages = Array.from(allImages).filter((img) => {
          const rect = img.getBoundingClientRect();
          return (
            rect.width > 0 &&
            rect.height > 0 &&
            window.getComputedStyle(img).display !== 'none' &&
            img.offsetParent !== null
          );
        });
    
        const total = visibleImages.length;
        if (total === 0) return; // Prevent division by zero
    
        const anglePerImage = 360 / total;
        const centerAngle = 0;
        const radius = 200;
        let closestImage = null;
        let closestDelta = Infinity;
    
        visibleImages.forEach((img, i) => {
          const angle = (anglePerImage * i + rotationOffset) % 360;
          const delta = Math.abs(((angle - centerAngle + 540) % 360) - 180);
          if (delta < closestDelta) {
            closestDelta = delta;
            closestImage = img;
          }
        });
    
        visibleImages.forEach((img, i) => {
          const angle = (anglePerImage * i + rotationOffset) % 360;
          const isCentered = img === closestImage;
          const scale = isCentered ? 2 : 1.2;
          const z = isCentered ? radius + 50 : radius;
          const zIndex = isCentered ? 100 : 0;
    
          img.style.transform = `
          rotateY(${angle}deg)
          translateZ(${z}px)
          scale(${scale})
        `;
          img.style.zIndex = String(zIndex);
        });
      }, [image, rotationOffset]); // Added image to dependencies as it affects which set is visible
    
      // --- Prepare all image elements for render ---
      const allImageElements = [];
      for (let themeIndex = 0; themeIndex < imageThemes.length; themeIndex++) {
        for (let i = 1; i <= imagesNum[themeIndex]; i++) {
          const lowSrc = `/pictures/low/${imageThemes[themeIndex]}/${i}.jpg`;
          const midSrc = `/pictures/mid/${imageThemes[themeIndex]}/${i}.jpg`; // Server-side optimized mid-res
          const highSrc = `/pictures/high/${imageThemes[themeIndex]}/${i}.jpg`; // Server-side optimized high-res
    
          allImageElements.push(
            <img
              key={`img-${themeIndex}-${i}`}
              src={lowSrc} // Start with low quality
              className={`VitrineImg-${themeIndex}-${i} VistrineImgs`}
              style={{ display: image === themeIndex ? 'block' : 'none' }}
              // Custom data attributes for Intersection Observer
              data-theme-index={themeIndex}
              data-img-number={i}
              data-mid-src={midSrc}
              data-high-src={highSrc}
              data-mid-loaded="false"
              data-high-loaded="false"
            />
          );
        }
      }
    
      return (
        <>
          <div
            className="loader"
            style={
              isLoadingInitial
                ? { zIndex: '9999', animationName: 'LoaderArrive', opacity: 1 }
                : { zIndex: '-9999', animationName: 'loaderDisapear', opacity: 0 }
            }
          >
            Chargement des images… {loadedLowResCount} / {totalLowResImages}
          </div>
    
          {/* Conditionally render Canvas to prevent unnecessary mounts/unmounts */}
          {showCanvas && (
            <Canvas
              style={{
                width: '100vw',
                height: 'calc(100vh - 100px)',
                marginTop: '0',
              }}
              onClick={() => {
                // Your existing logic for imagesRot and rotation
                const imagesRot = [0, 6.28311 / 3 + 0.2, 6.28311 / 1.5];
                rotation = imagesRot[image]; // `rotation` is a global var, consider making it state or ref
                setImagesOpen(!imagesOpen);
                console.log('hello world');
              }}
              camera={{ position: [0, 5, 0], fov: 50 }}
            >
              <ambientLight intensity={0.25} />
              <pointLight position={[0, 5, 5]} intensity={0.25} />
              <pointLight position={[5, 5, 10]} intensity={0.25} />
              <pointLight position={[5, 5, 5]} intensity={0.25} />
              <pointLight position={[-5, 5, -5]} intensity={0.25} />
              {/* Ensure Vitrine component is memoized or optimized if it causes re-renders */}
              <Vitrine
                position={[-1, 0, 0]}
                image={image}
                setImage={setImage}
                imagesOpen={imagesOpen} // Corrected prop name
              />
            </Canvas>
          )}
    
          <div
            className="VitrineImages"
            style={imagesOpen ? { opacity: '1' } : { opacity: '0' }}
          >
            <div className="carousel-3d">{allImageElements}</div>
          </div>
        </>
      );
    }
    
    // Global variable, consider making it a ref within VitrineCanvas or Vitrine
    // if it causes issues with strict mode or concurrent rendering
    var rotation = 0;
    var tBefor = 0;
    
    function Vitrine({ image, setImage, imagesOpen }) { // Corrected prop name
      const cubeRef = useRef();
    
      useFrame(({ clock }) => {
        cubeRef.current.position.y = 1;
        cubeRef.current.position.z = 0.5;
        const t = clock.getElapsedTime();
        var tour = Math.floor((cubeRef.current.rotation.y + 1) / 6.283118);
        var beforImage = image; // Misspelled 'beforeImage'
        // This calculation of newImage based on rotation seems tied to your specific 3D setup
        const newImage = Math.floor(
          (cubeRef.current.rotation.y + 1 - tour * 6.283118) / 6.283119 / 0.33
        );
    
        if (newImage !== image) {
          setImage(newImage);
        }
        cubeRef.current.scale.set(0.45, 0.45, 0.45);
        cubeRef.current.rotation.x = (3.14 / 2) * 3;
        var arriveOnImage = true;
        if (!arriveOnImage) { // This logic branch seems to always be false
          arriveOnImage = beforImage !== image; // Corrected comparison
        }
        if (
          tour * 6.283118 + (image - 1) * (3.14 / 1.5) + 2.5 <=
          cubeRef.current.rotation.y + 1
        ) {
          arriveOnImage = false;
        }
    
        if (cubeRef.current) {
          if (!imagesOpen) { // Corrected prop name
            if (arriveOnImage) {
              rotation += 1 * (t - tBefor);
            } else {
              rotation += 0.75 * (t - tBefor);
            }
          }
          cubeRef.current.rotation.y = rotation;
        }
        tBefor = t;
      });
    
      return (
        <mesh ref={cubeRef}>
          <MyModel />
        </mesh>
      );
    }