Buckets:
| import { useRef, useState, useEffect, useMemo } from 'react'; | |
| import { Canvas, useFrame, useThree } from '@react-three/fiber'; | |
| import { OrbitControls } from '@react-three/drei'; | |
| import * as THREE from 'three'; | |
| // Fonction pour convertir une valeur en couleur | |
| function valueToColor(value, min, max) { | |
| const t = Math.max(0, Math.min(1, (value - min) / (max - min))); | |
| let r, g, b; | |
| if (t < 0.25) { | |
| r = 0; g = 4 * t; b = 1; | |
| } else if (t < 0.5) { | |
| r = 0; g = 1; b = 1 - 4 * (t - 0.25); | |
| } else if (t < 0.75) { | |
| r = 4 * (t - 0.5); g = 1; b = 0; | |
| } else { | |
| r = 1; g = 1 - 4 * (t - 0.75); b = 0; | |
| } | |
| return [r, g, b]; | |
| } | |
| // Composant animé | |
| function AnimatedMesh({ meshData, frames, solutionRange, currentFrame, elevationScale }) { | |
| const meshRef = useRef(); | |
| const geometryRef = useRef(); | |
| const { min, max } = solutionRange; | |
| // Créer la géométrie initiale | |
| const geometry = useMemo(() => { | |
| const geom = new THREE.BufferGeometry(); | |
| const vertices = []; | |
| const colors = []; | |
| meshData.vertices.forEach(([x, y]) => { | |
| vertices.push(x, y, 0); | |
| colors.push(0, 0, 1); // Bleu initial | |
| }); | |
| const indices = []; | |
| meshData.triangles.forEach(tri => indices.push(...tri)); | |
| geom.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); | |
| geom.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); | |
| geom.setIndex(indices); | |
| geom.computeVertexNormals(); | |
| return geom; | |
| }, [meshData]); | |
| // Mettre à jour la géométrie à chaque frame | |
| useEffect(() => { | |
| if (!frames || !frames[currentFrame]) return; | |
| const positions = geometry.attributes.position.array; | |
| const colors = geometry.attributes.color.array; | |
| const frameData = frames[currentFrame]; | |
| meshData.vertices.forEach(([x, y], i) => { | |
| const value = frameData[i] || 0; | |
| positions[i * 3 + 2] = value * elevationScale; // Z = valeur * échelle | |
| const [r, g, b] = valueToColor(value, min, max); | |
| colors[i * 3] = r; | |
| colors[i * 3 + 1] = g; | |
| colors[i * 3 + 2] = b; | |
| }); | |
| geometry.attributes.position.needsUpdate = true; | |
| geometry.attributes.color.needsUpdate = true; | |
| geometry.computeVertexNormals(); | |
| }, [currentFrame, frames, meshData, min, max, elevationScale, geometry]); | |
| return ( | |
| <mesh ref={meshRef} geometry={geometry}> | |
| <meshStandardMaterial vertexColors side={THREE.DoubleSide} /> | |
| </mesh> | |
| ); | |
| } | |
| // Barre de couleur | |
| function ColorBar({ min, max }) { | |
| const segments = 50; | |
| return ( | |
| <div className="colorbar"> | |
| <div className="colorbar-gradient"> | |
| {Array.from({ length: segments }).map((_, i) => { | |
| const t = i / (segments - 1); | |
| const [r, g, b] = valueToColor(t, 0, 1); | |
| return ( | |
| <div | |
| key={i} | |
| style={{ | |
| flex: 1, | |
| backgroundColor: `rgb(${r*255}, ${g*255}, ${b*255})` | |
| }} | |
| /> | |
| ); | |
| })} | |
| </div> | |
| <div className="colorbar-labels"> | |
| <span>{min?.toFixed(4)}</span> | |
| <span>{((min + max) / 2)?.toFixed(4)}</span> | |
| <span>{max?.toFixed(4)}</span> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default function AnimationPlayer({ meshData, frames, solutionRange }) { | |
| const [currentFrame, setCurrentFrame] = useState(0); | |
| const [isPlaying, setIsPlaying] = useState(false); | |
| const [playbackSpeed, setPlaybackSpeed] = useState(1); | |
| const [elevationScale, setElevationScale] = useState(0.3); | |
| const [loop, setLoop] = useState(true); | |
| const totalFrames = frames?.length || 0; | |
| // Debug logging | |
| console.log('AnimationPlayer props:', { meshData, frames, solutionRange }); | |
| console.log('Total frames:', totalFrames); | |
| // Animation automatique | |
| useEffect(() => { | |
| if (!isPlaying) return; | |
| const interval = setInterval(() => { | |
| setCurrentFrame(prev => { | |
| if (prev >= totalFrames - 1) { | |
| if (loop) return 0; | |
| setIsPlaying(false); | |
| return prev; | |
| } | |
| return prev + 1; | |
| }); | |
| }, 100 / playbackSpeed); | |
| return () => clearInterval(interval); | |
| }, [isPlaying, totalFrames, playbackSpeed, loop]); | |
| if (!meshData || !frames || frames.length === 0) { | |
| console.log('AnimationPlayer: données manquantes', { | |
| hasMeshData: !!meshData, | |
| hasFrames: !!frames, | |
| framesLength: frames?.length | |
| }); | |
| return <div className="placeholder">Aucune animation disponible</div>; | |
| } | |
| if (!solutionRange || solutionRange.min === undefined || solutionRange.max === undefined) { | |
| console.error('AnimationPlayer: solutionRange invalide', solutionRange); | |
| return <div className="placeholder">Données d'animation invalides</div>; | |
| } | |
| const bounds = meshData.bounds; | |
| const cx = (bounds.x_min + bounds.x_max) / 2; | |
| const cy = (bounds.y_min + bounds.y_max) / 2; | |
| return ( | |
| <div className="animation-player"> | |
| <div className="viewer-controls"> | |
| <div className="playback-controls"> | |
| <button onClick={() => setCurrentFrame(0)} title="Début"> | |
| ⏮ | |
| </button> | |
| <button onClick={() => setCurrentFrame(Math.max(0, currentFrame - 1))} title="Précédent"> | |
| ◀ | |
| </button> | |
| <button onClick={() => setIsPlaying(!isPlaying)} title={isPlaying ? 'Pause' : 'Lecture'}> | |
| {isPlaying ? '⏸' : '▶'} | |
| </button> | |
| <button onClick={() => setCurrentFrame(Math.min(totalFrames - 1, currentFrame + 1))} title="Suivant"> | |
| ▶ | |
| </button> | |
| <button onClick={() => setCurrentFrame(totalFrames - 1)} title="Fin"> | |
| ⏭ | |
| </button> | |
| </div> | |
| <div className="frame-info"> | |
| Frame: {currentFrame + 1} / {totalFrames} | |
| </div> | |
| <label className="loop-toggle"> | |
| <input | |
| type="checkbox" | |
| checked={loop} | |
| onChange={(e) => setLoop(e.target.checked)} | |
| /> | |
| Boucle | |
| </label> | |
| </div> | |
| <div className="canvas-container"> | |
| <Canvas camera={{ position: [cx + 2, cy + 1, 2], fov: 50 }}> | |
| <color attach="background" args={['#1a1a2e']} /> | |
| <ambientLight intensity={0.6} /> | |
| <directionalLight position={[5, 5, 5]} intensity={0.8} /> | |
| <directionalLight position={[-5, -5, 5]} intensity={0.4} /> | |
| <AnimatedMesh | |
| meshData={meshData} | |
| frames={frames} | |
| solutionRange={solutionRange} | |
| currentFrame={currentFrame} | |
| elevationScale={elevationScale} | |
| /> | |
| <OrbitControls | |
| target={[cx, cy, 0]} | |
| enablePan={true} | |
| enableZoom={true} | |
| enableRotate={true} | |
| /> | |
| <gridHelper | |
| args={[4, 20, '#333', '#222']} | |
| position={[cx, cy, -0.01]} | |
| rotation={[Math.PI / 2, 0, 0]} | |
| /> | |
| </Canvas> | |
| <ColorBar min={solutionRange.min} max={solutionRange.max} /> | |
| </div> | |
| <div className="animation-controls"> | |
| <div className="control-group"> | |
| <label>Timeline</label> | |
| <input | |
| type="range" | |
| min="0" | |
| max={totalFrames - 1} | |
| value={currentFrame} | |
| onChange={(e) => setCurrentFrame(parseInt(e.target.value))} | |
| className="timeline-slider" | |
| /> | |
| </div> | |
| <div className="control-row"> | |
| <div className="control-group"> | |
| <label>Vitesse: {playbackSpeed}x</label> | |
| <input | |
| type="range" | |
| min="0.25" | |
| max="4" | |
| step="0.25" | |
| value={playbackSpeed} | |
| onChange={(e) => setPlaybackSpeed(parseFloat(e.target.value))} | |
| /> | |
| </div> | |
| <div className="control-group"> | |
| <label>Élévation: {elevationScale.toFixed(2)}</label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="1" | |
| step="0.05" | |
| value={elevationScale} | |
| onChange={(e) => setElevationScale(parseFloat(e.target.value))} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 10.1 kB
- Xet hash:
- a58f9458dbad442fc005b125cc598169ef231c2d23c7defeebcd1ca3a8bac26d
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.