download
raw
10.1 kB
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.