Add mouse cells
This commit is contained in:
parent
da843ed1c4
commit
699bf4c62e
BIN
embryo-backend/Data/GSM9046243_Embryo_E7.5_stereo_rep1.h5ad
(Stored with Git LFS)
Normal file
BIN
embryo-backend/Data/GSM9046243_Embryo_E7.5_stereo_rep1.h5ad
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
embryo-backend/Data/GSM9046244_Embryo_E7.5_stereo_rep2.h5ad
(Stored with Git LFS)
Normal file
BIN
embryo-backend/Data/GSM9046244_Embryo_E7.5_stereo_rep2.h5ad
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
embryo-backend/Data/GSM9046245_Embryo_E7.75_stereo_rep1.h5ad
(Stored with Git LFS)
Normal file
BIN
embryo-backend/Data/GSM9046245_Embryo_E7.75_stereo_rep1.h5ad
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
embryo-backend/Data/GSM9046246_Embryo_E7.75_stereo_rep2.h5ad
(Stored with Git LFS)
Normal file
BIN
embryo-backend/Data/GSM9046246_Embryo_E7.75_stereo_rep2.h5ad
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
embryo-backend/Data/GSM9046247_Embryo_E8.0_stereo_rep1.h5ad
(Stored with Git LFS)
Normal file
BIN
embryo-backend/Data/GSM9046247_Embryo_E8.0_stereo_rep1.h5ad
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
embryo-backend/Data/GSM9046248_Embryo_E8.0_stereo_rep2.h5ad
(Stored with Git LFS)
Normal file
BIN
embryo-backend/Data/GSM9046248_Embryo_E8.0_stereo_rep2.h5ad
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -1,170 +1,110 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||||
import "../style.css";
|
import "../style.css";
|
||||||
|
|
||||||
interface Point {
|
interface Point {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
z: number;
|
z: number;
|
||||||
value: number | string;
|
value: number | string;
|
||||||
}
|
}
|
||||||
|
interface CameraParameters {
|
||||||
const EmbryoGeneration: React.FC = () => {
|
fov: number;
|
||||||
const [cs7Data, setCs7Data] = useState<Point[]>([]);
|
near: number;
|
||||||
const [cs8Data, setCs8Data] = useState<Point[]>([]);
|
far: number;
|
||||||
const [cs75Data, setCs75Data] = useState<Point[]>([]);
|
position: THREE.Vector3;
|
||||||
const [loading, setLoading] = useState(true);
|
lookAt: THREE.Vector3;
|
||||||
const [error, setError] = useState("");
|
}
|
||||||
const [animationProgress, setAnimationProgress] = useState(0);
|
interface LayoutConfig {
|
||||||
const [isAnimating, setIsAnimating] = useState(false);
|
leftOffsetX: number;
|
||||||
const [animationSpeed, setAnimationSpeed] = useState(2);
|
rightOffsetX: number;
|
||||||
|
targetOffsetX?: number;
|
||||||
// 加载数据
|
cameraParams: CameraParameters;
|
||||||
useEffect(() => {
|
transFunc?: (a: Point) => Point;
|
||||||
const loadData = async () => {
|
}
|
||||||
setLoading(true);
|
interface AnimationSectionConfig {
|
||||||
setError("");
|
id: string;
|
||||||
try {
|
title: string;
|
||||||
const [cs7Res, cs8Res, cs75Res] = await Promise.all([
|
description: string;
|
||||||
axios.get("http://localhost:5000/api/cell", { params: { stage: "CS7" } }),
|
stages: {
|
||||||
axios.get("http://localhost:5000/api/cell", { params: { stage: "CS8" } }),
|
sourceLeft: string;
|
||||||
axios.get("http://localhost:5000/api/cell", { params: { stage: "CS7.5" } })
|
sourceRight: string;
|
||||||
]);
|
target: string;
|
||||||
setCs7Data(cs7Res.data.cells);
|
};
|
||||||
setCs8Data(cs8Res.data.cells);
|
layout: LayoutConfig;
|
||||||
setCs75Data(cs75Res.data.cells);
|
}
|
||||||
} catch (err: any) {
|
const ANIMATION_SECTIONS: AnimationSectionConfig[] = [
|
||||||
if (err.response && err.response.data?.error) {
|
{
|
||||||
setError(err.response.data.error);
|
id: "human-cs",
|
||||||
} else {
|
title: "Human Carnegie stages",
|
||||||
setError("Failed to fetch embryo data.");
|
description: "CS7 and CS8 point clouds converge into CS7.5 to highlight human embryonic development.",
|
||||||
}
|
stages: {
|
||||||
} finally {
|
sourceLeft: "CS7",
|
||||||
setLoading(false);
|
sourceRight: "CS8",
|
||||||
|
target: "CS7.5"
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
leftOffsetX: -0.5,
|
||||||
|
rightOffsetX: 0.5,
|
||||||
|
targetOffsetX: 0,
|
||||||
|
cameraParams: {
|
||||||
|
fov: 30,
|
||||||
|
near: 0.1,
|
||||||
|
far: 1000,
|
||||||
|
position: new THREE.Vector3(0, 0, 30),
|
||||||
|
lookAt: new THREE.Vector3(0, 0, 0)
|
||||||
}
|
}
|
||||||
};
|
|
||||||
loadData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 动画逻辑
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isAnimating || cs7Data.length === 0 || cs8Data.length === 0 || cs75Data.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const duration = 5000 / animationSpeed;
|
},
|
||||||
const startTime = Date.now();
|
{
|
||||||
const animate = () => {
|
id: "mouse-early",
|
||||||
const elapsed = Date.now() - startTime;
|
title: "Mouse early gastrulation",
|
||||||
const progress = Math.min(elapsed / duration, 1);
|
description: "Mouse 7.5 and 7.75 datasets merge toward stage 8.5, mirroring the human pipeline.",
|
||||||
setAnimationProgress(progress);
|
stages: {
|
||||||
if (progress < 1) {
|
// sourceLeft: "GSM9046243_Embryo_E7.5_stereo_rep1",
|
||||||
requestAnimationFrame(animate);
|
// sourceRight: "GSM9046247_Embryo_E8.0_stereo_rep1",
|
||||||
} else {
|
// target: "GSM9046245_Embryo_E7.75_stereo_rep1"
|
||||||
setIsAnimating(false);
|
sourceLeft: "GSM9046244_Embryo_E7.5_stereo_rep2",
|
||||||
setAnimationProgress(1);
|
sourceRight: "GSM9046248_Embryo_E8.0_stereo_rep2",
|
||||||
|
target: "GSM9046246_Embryo_E7.75_stereo_rep2"
|
||||||
|
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
leftOffsetX: -0.3,
|
||||||
|
rightOffsetX: 0.3,
|
||||||
|
targetOffsetX: -0.05,
|
||||||
|
cameraParams: {
|
||||||
|
fov: 30,
|
||||||
|
near: 0.1,
|
||||||
|
far: 1000,
|
||||||
|
position: new THREE.Vector3(-10, 0, 30),
|
||||||
|
lookAt: new THREE.Vector3(0, 0, 0)
|
||||||
|
},
|
||||||
|
transFunc: (p: Point) => {
|
||||||
|
return {
|
||||||
|
x: p.x / 50,
|
||||||
|
y: p.y / 50,
|
||||||
|
z: p.z / 60,
|
||||||
|
value: p.value
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
animate();
|
}
|
||||||
}, [isAnimating, cs7Data.length, cs8Data.length, cs75Data.length, animationSpeed]);
|
];
|
||||||
|
const EmbryoGeneration: React.FC = () => {
|
||||||
const startAnimation = () => {
|
|
||||||
setIsAnimating(true);
|
|
||||||
setAnimationProgress(0);
|
|
||||||
};
|
|
||||||
const resetAnimation = () => {
|
|
||||||
setIsAnimating(false);
|
|
||||||
setAnimationProgress(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1>Embryo Generation</h1>
|
<h1>Embryo Generation</h1>
|
||||||
<p className="page-description">
|
<p className="page-description">
|
||||||
Observe cell migration and merging during embryonic development. CS7, CS8, and CS7.5 point clouds are shown in one 3D view, with an animation simulating cells converging from both sides.
|
Observe cell migration and merging across human CS7/CS8/CS7.5 and mouse 7.5/7.75/8.5 datasets with the same control workflow.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-content">
|
<div className="page-content">
|
||||||
<div className="control-panel" style={{
|
{ANIMATION_SECTIONS.map((section) => (
|
||||||
display: "flex",
|
<EmbryoAnimationSection key={section.id} config={section} />
|
||||||
alignItems: "center",
|
))}
|
||||||
gap: "2rem",
|
|
||||||
marginBottom: "2rem",
|
|
||||||
padding: "1rem",
|
|
||||||
backgroundColor: "var(--bg-secondary)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
flexWrap: "wrap"
|
|
||||||
}}>
|
|
||||||
<button
|
|
||||||
onClick={startAnimation}
|
|
||||||
disabled={isAnimating || loading}
|
|
||||||
style={{
|
|
||||||
padding: "0.75rem 1.5rem",
|
|
||||||
backgroundColor: "var(--primary-color)",
|
|
||||||
color: "white",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "6px",
|
|
||||||
cursor: isAnimating || loading ? "not-allowed" : "pointer",
|
|
||||||
opacity: isAnimating || loading ? 0.6 : 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isAnimating ? "Animating..." : "Start animation"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={resetAnimation}
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
|
||||||
padding: "0.75rem 1.5rem",
|
|
||||||
backgroundColor: "var(--secondary-color)",
|
|
||||||
color: "white",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "6px",
|
|
||||||
cursor: loading ? "not-allowed" : "pointer",
|
|
||||||
opacity: loading ? 0.6 : 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
||||||
Animation speed:
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0.5"
|
|
||||||
max="3"
|
|
||||||
step="0.1"
|
|
||||||
value={animationSpeed}
|
|
||||||
onChange={(e) => setAnimationSpeed(parseFloat(e.target.value))}
|
|
||||||
style={{ width: "100px" }}
|
|
||||||
/>
|
|
||||||
<span>{animationSpeed}x</span>
|
|
||||||
</label>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
||||||
Progress: {Math.round(animationProgress * 100)}%
|
|
||||||
<div style={{
|
|
||||||
width: "100px",
|
|
||||||
height: "8px",
|
|
||||||
backgroundColor: "var(--bg-tertiary)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
overflow: "hidden"
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
width: `${animationProgress * 100}%`,
|
|
||||||
height: "100%",
|
|
||||||
backgroundColor: "var(--primary-color)",
|
|
||||||
transition: "width 0.1s ease"
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{error && <div className="error-message" style={{ marginBottom: "2rem" }}>{error}</div>}
|
|
||||||
<UnifiedEmbryoAnimation
|
|
||||||
cs7Data={cs7Data}
|
|
||||||
cs8Data={cs8Data}
|
|
||||||
cs75Data={cs75Data}
|
|
||||||
animationProgress={animationProgress}
|
|
||||||
/>
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h2>User Guide</h2>
|
<h2>User Guide</h2>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", gap: "2rem", marginTop: "1.5rem" }}>
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", gap: "2rem", marginTop: "1.5rem" }}>
|
||||||
@ -191,21 +131,214 @@ const EmbryoGeneration: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface UnifiedEmbryoAnimationProps {
|
interface EmbryoAnimationSectionProps {
|
||||||
cs7Data: Point[];
|
config: AnimationSectionConfig;
|
||||||
cs8Data: Point[];
|
|
||||||
cs75Data: Point[];
|
|
||||||
animationProgress: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data, cs8Data, cs75Data, animationProgress }) => {
|
const EmbryoAnimationSection: React.FC<EmbryoAnimationSectionProps> = ({ config }) => {
|
||||||
|
const { stages, title, description, layout } = config;
|
||||||
|
const { sourceLeft, sourceRight, target } = stages;
|
||||||
|
const [sourceLeftData, setSourceLeftData] = useState<Point[]>([]);
|
||||||
|
const [sourceRightData, setSourceRightData] = useState<Point[]>([]);
|
||||||
|
const [targetData, setTargetData] = useState<Point[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [animationProgress, setAnimationProgress] = useState(0);
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
const [animationSpeed, setAnimationSpeed] = useState(2);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const [leftRes, rightRes, targetRes] = await Promise.all([
|
||||||
|
axios.get("http://localhost:5000/api/cell", { params: { stage: sourceLeft } }),
|
||||||
|
axios.get("http://localhost:5000/api/cell", { params: { stage: sourceRight } }),
|
||||||
|
axios.get("http://localhost:5000/api/cell", { params: { stage: target } })
|
||||||
|
]);
|
||||||
|
setSourceLeftData(leftRes.data.cells || []);
|
||||||
|
setSourceRightData(rightRes.data.cells || []);
|
||||||
|
setTargetData(targetRes.data.cells || []);
|
||||||
|
setAnimationProgress(0);
|
||||||
|
setIsAnimating(false);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response && err.response.data?.error) {
|
||||||
|
setError(err.response.data.error);
|
||||||
|
} else {
|
||||||
|
setError("Failed to fetch embryo data.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
}, [sourceLeft, sourceRight, target]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAnimating || sourceLeftData.length === 0 || sourceRightData.length === 0 || targetData.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duration = 5000 / animationSpeed;
|
||||||
|
const startTime = Date.now();
|
||||||
|
const animate = () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
setAnimationProgress(progress);
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
} else {
|
||||||
|
setIsAnimating(false);
|
||||||
|
setAnimationProgress(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
animate();
|
||||||
|
}, [isAnimating, sourceLeftData.length, sourceRightData.length, targetData.length, animationSpeed]);
|
||||||
|
|
||||||
|
const startAnimation = () => {
|
||||||
|
setIsAnimating(true);
|
||||||
|
setAnimationProgress(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAnimation = () => {
|
||||||
|
setIsAnimating(false);
|
||||||
|
setAnimationProgress(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const datasetLabel = `${sourceLeft} + ${sourceRight} -> ${target}`;
|
||||||
|
const progressPercent = Math.round(animationProgress * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section" style={{ marginBottom: "3rem" }}>
|
||||||
|
<div style={{ marginBottom: "1.5rem" }}>
|
||||||
|
<h2 style={{ marginBottom: "0.5rem" }}>{title}</h2>
|
||||||
|
<p className="page-description" style={{ marginBottom: "0.25rem" }}>{description}</p>
|
||||||
|
<p style={{ color: "var(--text-secondary)", margin: 0 }}>Dataset: {datasetLabel}</p>
|
||||||
|
</div>
|
||||||
|
<div className="control-panel" style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "2rem",
|
||||||
|
marginBottom: "2rem",
|
||||||
|
padding: "1rem",
|
||||||
|
backgroundColor: "var(--bg-secondary)",
|
||||||
|
borderRadius: "8px",
|
||||||
|
flexWrap: "wrap"
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={startAnimation}
|
||||||
|
disabled={isAnimating || loading}
|
||||||
|
style={{
|
||||||
|
padding: "0.75rem 1.5rem",
|
||||||
|
backgroundColor: "var(--primary-color)",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "6px",
|
||||||
|
cursor: isAnimating || loading ? "not-allowed" : "pointer",
|
||||||
|
opacity: isAnimating || loading ? 0.6 : 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isAnimating ? "Animating..." : "Start animation"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={resetAnimation}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
padding: "0.75rem 1.5rem",
|
||||||
|
backgroundColor: "var(--secondary-color)",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "6px",
|
||||||
|
cursor: loading ? "not-allowed" : "pointer",
|
||||||
|
opacity: loading ? 0.6 : 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||||
|
Animation speed:
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.5"
|
||||||
|
max="3"
|
||||||
|
step="0.1"
|
||||||
|
value={animationSpeed}
|
||||||
|
onChange={(e) => setAnimationSpeed(parseFloat(e.target.value))}
|
||||||
|
style={{ width: "100px" }}
|
||||||
|
/>
|
||||||
|
<span>{animationSpeed}x</span>
|
||||||
|
</label>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||||
|
Progress: {progressPercent}%
|
||||||
|
<div style={{
|
||||||
|
width: "100px",
|
||||||
|
height: "8px",
|
||||||
|
backgroundColor: "var(--bg-tertiary)",
|
||||||
|
borderRadius: "4px",
|
||||||
|
overflow: "hidden"
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: `${animationProgress * 100}%`,
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "var(--primary-color)",
|
||||||
|
transition: "width 0.1s ease"
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && <div className="error-message" style={{ marginBottom: "2rem" }}>{error}</div>}
|
||||||
|
<UnifiedEmbryoAnimation
|
||||||
|
sourceLeftData={sourceLeftData}
|
||||||
|
sourceRightData={sourceRightData}
|
||||||
|
targetData={targetData}
|
||||||
|
animationProgress={animationProgress}
|
||||||
|
layout={layout}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
interface UnifiedEmbryoAnimationProps {
|
||||||
|
sourceLeftData: Point[];
|
||||||
|
sourceRightData: Point[];
|
||||||
|
targetData: Point[];
|
||||||
|
animationProgress: number;
|
||||||
|
layout: LayoutConfig;
|
||||||
|
}
|
||||||
|
interface TargetAnimationState {
|
||||||
|
positionsAttr: THREE.BufferAttribute;
|
||||||
|
alphaAttr: THREE.BufferAttribute;
|
||||||
|
fromPositions: Float32Array;
|
||||||
|
toPositions: Float32Array;
|
||||||
|
delays: Float32Array;
|
||||||
|
moveDuration: number;
|
||||||
|
fadeInDuration: number;
|
||||||
|
}
|
||||||
|
const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ sourceLeftData, sourceRightData, targetData, animationProgress, layout }) => {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
||||||
const sceneRef = useRef<THREE.Scene | null>(null);
|
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||||
const controlsRef = useRef<any>(null);
|
const controlsRef = useRef<any>(null);
|
||||||
const animationIdRef = useRef<number | null>(null);
|
const animationIdRef = useRef<number | null>(null);
|
||||||
|
const animationStateRef = useRef<TargetAnimationState | null>(null);
|
||||||
|
|
||||||
|
const { leftOffsetX, rightOffsetX, targetOffsetX = 0, cameraParams, transFunc = null } = layout;
|
||||||
|
|
||||||
|
if (transFunc) {
|
||||||
|
for (let i = 0; i < sourceLeftData.length; i++) {
|
||||||
|
if (i < 5) console.log(`Source Left Point ${i}: original (${sourceLeftData[i].x.toFixed(2)}, ${sourceLeftData[i].y.toFixed(2)}, ${sourceLeftData[i].z.toFixed(2)})`);
|
||||||
|
sourceLeftData[i] = transFunc(sourceLeftData[i]);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < sourceRightData.length; i++) {
|
||||||
|
sourceRightData[i] = transFunc(sourceRightData[i]);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < targetData.length; i++) {
|
||||||
|
targetData[i] = transFunc(targetData[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 颜色映射
|
|
||||||
const getColorMap = (allData: Point[]) => {
|
const getColorMap = (allData: Point[]) => {
|
||||||
const cellTypes = Array.from(new Set(allData.map(p => p.value as string))).sort();
|
const cellTypes = Array.from(new Set(allData.map(p => p.value as string))).sort();
|
||||||
const colors = [
|
const colors = [
|
||||||
@ -221,9 +354,37 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
|
|||||||
return colorMap;
|
return colorMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyAnimationProgress = useCallback((progress: number) => {
|
||||||
|
const animationState = animationStateRef.current;
|
||||||
|
if (!animationState) return;
|
||||||
|
const { positionsAttr, alphaAttr, fromPositions, toPositions, delays, moveDuration, fadeInDuration } = animationState;
|
||||||
|
const positionsArray = positionsAttr.array as Float32Array;
|
||||||
|
const alphaArray = alphaAttr.array as Float32Array;
|
||||||
|
const total = delays.length;
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const delay = delays[i];
|
||||||
|
let pointProgress = (progress - delay) / moveDuration;
|
||||||
|
pointProgress = Math.max(0, Math.min(1, pointProgress));
|
||||||
|
const easeProgress = pointProgress < 0.5
|
||||||
|
? 2 * pointProgress * pointProgress
|
||||||
|
: 1 - Math.pow(-2 * pointProgress + 2, 2) / 2;
|
||||||
|
const baseIndex = i * 3;
|
||||||
|
positionsArray[baseIndex] = fromPositions[baseIndex] + (toPositions[baseIndex] - fromPositions[baseIndex]) * easeProgress;
|
||||||
|
positionsArray[baseIndex + 1] = fromPositions[baseIndex + 1] + (toPositions[baseIndex + 1] - fromPositions[baseIndex + 1]) * easeProgress;
|
||||||
|
positionsArray[baseIndex + 2] = fromPositions[baseIndex + 2] + (toPositions[baseIndex + 2] - fromPositions[baseIndex + 2]) * easeProgress;
|
||||||
|
|
||||||
|
const fadeProgress = Math.min(pointProgress / fadeInDuration, 1);
|
||||||
|
alphaArray[i] = fadeProgress;
|
||||||
|
}
|
||||||
|
positionsAttr.needsUpdate = true;
|
||||||
|
alphaAttr.needsUpdate = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
// 清理
|
|
||||||
|
animationStateRef.current = null;
|
||||||
|
|
||||||
if (animationIdRef.current !== null) {
|
if (animationIdRef.current !== null) {
|
||||||
cancelAnimationFrame(animationIdRef.current);
|
cancelAnimationFrame(animationIdRef.current);
|
||||||
animationIdRef.current = null;
|
animationIdRef.current = null;
|
||||||
@ -232,16 +393,22 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
|
|||||||
rendererRef.current.dispose();
|
rendererRef.current.dispose();
|
||||||
rendererRef.current = null;
|
rendererRef.current = null;
|
||||||
}
|
}
|
||||||
while (containerRef.current.firstChild) {
|
if (controlsRef.current) {
|
||||||
containerRef.current.removeChild(containerRef.current.firstChild);
|
controlsRef.current.dispose();
|
||||||
|
controlsRef.current = null;
|
||||||
}
|
}
|
||||||
// 初始化
|
if (containerRef.current) {
|
||||||
|
while (containerRef.current.firstChild) {
|
||||||
|
containerRef.current.removeChild(containerRef.current.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const width = containerRef.current.clientWidth;
|
const width = containerRef.current.clientWidth;
|
||||||
const height = containerRef.current.clientHeight;
|
const height = containerRef.current.clientHeight;
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
sceneRef.current = scene;
|
sceneRef.current = scene;
|
||||||
const camera = new THREE.PerspectiveCamera(30, width / height, 0.1, 1000);
|
const camera = new THREE.PerspectiveCamera(cameraParams.fov, width / height, cameraParams.near, cameraParams.far);
|
||||||
camera.position.set(0, 0, 30);
|
camera.position.set(cameraParams.position.x, cameraParams.position.y, cameraParams.position.z);
|
||||||
camera.lookAt(0, 0, 0);
|
camera.lookAt(0, 0, 0);
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
@ -250,42 +417,28 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
|
|||||||
const controls = new OrbitControls(camera, renderer.domElement);
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true;
|
||||||
controlsRef.current = controls;
|
controlsRef.current = controls;
|
||||||
// 不添加坐标轴
|
|
||||||
|
|
||||||
// 颜色映射
|
const disposableMaterials: THREE.Material[] = [];
|
||||||
const allData = [...cs7Data, ...cs8Data, ...cs75Data];
|
const disposableGeometries: THREE.BufferGeometry[] = [];
|
||||||
|
const disposableTextures: THREE.Texture[] = [];
|
||||||
|
const pointTexture = createTexture();
|
||||||
|
disposableTextures.push(pointTexture);
|
||||||
|
|
||||||
|
const allData = [...sourceLeftData, ...sourceRightData, ...targetData];
|
||||||
const colorMap = getColorMap(allData);
|
const colorMap = getColorMap(allData);
|
||||||
|
const allPositions: number[] = [];
|
||||||
|
const recordPosition = (x: number, y: number, z: number) => {
|
||||||
|
allPositions.push(x, y, z);
|
||||||
|
};
|
||||||
|
|
||||||
// 计算所有点的包围盒,自动居中和缩放
|
if (sourceLeftData.length > 0) {
|
||||||
let allPositions: number[] = [];
|
|
||||||
cs7Data.forEach(p => { allPositions.push(p.x - 0.5, p.y, p.z); });
|
|
||||||
cs8Data.forEach(p => { allPositions.push(p.x + 0.5,p.y, p.z); });
|
|
||||||
cs75Data.forEach((p, i) => {
|
|
||||||
const fromX = i % 2 === 0 ? p.x - 0.5 : p.x + 0.5;
|
|
||||||
allPositions.push(fromX, p.y, p.z);
|
|
||||||
allPositions.push(p.x, p.y, p.z);
|
|
||||||
});
|
|
||||||
if (allPositions.length > 0) {
|
|
||||||
const posArr = new Float32Array(allPositions);
|
|
||||||
const box = new THREE.Box3().setFromArray(posArr);
|
|
||||||
const center = new THREE.Vector3();
|
|
||||||
box.getCenter(center);
|
|
||||||
const size = new THREE.Vector3();
|
|
||||||
box.getSize(size);
|
|
||||||
// 让最大边长适配canvas
|
|
||||||
const maxDim = Math.max(size.x, size.y, size.z);
|
|
||||||
const fitScale = 32 / maxDim; // 32为经验缩放系数
|
|
||||||
scene.position.set(-center.x, -center.y, -center.z);
|
|
||||||
scene.scale.set(fitScale, fitScale, fitScale);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CS7点云(左,静态)
|
|
||||||
if (cs7Data.length > 0) {
|
|
||||||
const geometry = new THREE.BufferGeometry();
|
const geometry = new THREE.BufferGeometry();
|
||||||
const positions: number[] = [];
|
const positions: number[] = [];
|
||||||
const colors: number[] = [];
|
const colors: number[] = [];
|
||||||
cs7Data.forEach((p) => {
|
sourceLeftData.forEach((p) => {
|
||||||
positions.push(p.x - 0.5, p.y, p.z);
|
const shiftedX = p.x + leftOffsetX;
|
||||||
|
positions.push(shiftedX, p.y, p.z);
|
||||||
|
recordPosition(shiftedX, p.y, p.z);
|
||||||
const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC');
|
const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC');
|
||||||
colors.push(c.r, c.g, c.b);
|
colors.push(c.r, c.g, c.b);
|
||||||
});
|
});
|
||||||
@ -299,35 +452,22 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
|
|||||||
alphaTest: 0.5,
|
alphaTest: 0.5,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
blending: THREE.NormalBlending,
|
blending: THREE.NormalBlending,
|
||||||
map: (() => {
|
map: pointTexture
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = 64;
|
|
||||||
canvas.height = 64;
|
|
||||||
const ctx = canvas.getContext('2d')!;
|
|
||||||
ctx.clearRect(0, 0, 64, 64);
|
|
||||||
// 绘制带有alpha渐变的圆形
|
|
||||||
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 30);
|
|
||||||
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
|
||||||
gradient.addColorStop(0.98, 'rgba(255,255,255,1)');
|
|
||||||
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(32, 32, 30, 0, 2 * Math.PI);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fillStyle = gradient;
|
|
||||||
ctx.fill();
|
|
||||||
return new THREE.CanvasTexture(canvas);
|
|
||||||
})()
|
|
||||||
});
|
});
|
||||||
const points = new THREE.Points(geometry, material);
|
const points = new THREE.Points(geometry, material);
|
||||||
scene.add(points);
|
scene.add(points);
|
||||||
|
disposableGeometries.push(geometry);
|
||||||
|
disposableMaterials.push(material);
|
||||||
}
|
}
|
||||||
// CS8点云(右,静态)
|
|
||||||
if (cs8Data.length > 0) {
|
if (sourceRightData.length > 0) {
|
||||||
const geometry = new THREE.BufferGeometry();
|
const geometry = new THREE.BufferGeometry();
|
||||||
const positions: number[] = [];
|
const positions: number[] = [];
|
||||||
const colors: number[] = [];
|
const colors: number[] = [];
|
||||||
cs8Data.forEach((p) => {
|
sourceRightData.forEach((p) => {
|
||||||
positions.push(p.x + 0.5, p.y, p.z);
|
const shiftedX = p.x + rightOffsetX;
|
||||||
|
positions.push(shiftedX, p.y, p.z);
|
||||||
|
recordPosition(shiftedX, p.y, p.z);
|
||||||
const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC');
|
const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC');
|
||||||
colors.push(c.r, c.g, c.b);
|
colors.push(c.r, c.g, c.b);
|
||||||
});
|
});
|
||||||
@ -341,122 +481,118 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
|
|||||||
alphaTest: 0.5,
|
alphaTest: 0.5,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
blending: THREE.NormalBlending,
|
blending: THREE.NormalBlending,
|
||||||
map: (() => {
|
map: pointTexture
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = 64;
|
|
||||||
canvas.height = 64;
|
|
||||||
const ctx = canvas.getContext('2d')!;
|
|
||||||
ctx.clearRect(0, 0, 64, 64);
|
|
||||||
// 绘制带有alpha渐变的圆形
|
|
||||||
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 30);
|
|
||||||
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
|
||||||
gradient.addColorStop(0.98, 'rgba(255,255,255,1)');
|
|
||||||
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(32, 32, 30, 0, 2 * Math.PI);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fillStyle = gradient;
|
|
||||||
ctx.fill();
|
|
||||||
return new THREE.CanvasTexture(canvas);
|
|
||||||
})()
|
|
||||||
});
|
});
|
||||||
const points = new THREE.Points(geometry, material);
|
const points = new THREE.Points(geometry, material);
|
||||||
scene.add(points);
|
scene.add(points);
|
||||||
|
disposableGeometries.push(geometry);
|
||||||
|
disposableMaterials.push(material);
|
||||||
}
|
}
|
||||||
// CS7.5点云(动画)
|
|
||||||
if (cs75Data.length > 0) {
|
const pickFallbackPoint = (preferred: Point[], alternative: Point[], index: number, defaultPoint: Point): Point => {
|
||||||
|
if (preferred.length > 0) {
|
||||||
|
return preferred[index % preferred.length];
|
||||||
|
}
|
||||||
|
if (alternative.length > 0) {
|
||||||
|
return alternative[index % alternative.length];
|
||||||
|
}
|
||||||
|
return defaultPoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (targetData.length > 0) {
|
||||||
const geometry = new THREE.BufferGeometry();
|
const geometry = new THREE.BufferGeometry();
|
||||||
const positions: number[] = [];
|
const n = targetData.length;
|
||||||
const colors: number[] = [];
|
const positionsArray = new Float32Array(n * 3);
|
||||||
const alphas: number[] = [];
|
const colorsArray = new Float32Array(n * 3);
|
||||||
const n = cs75Data.length;
|
const alphasArray = new Float32Array(n);
|
||||||
const moveDuration = 0.4; // 每个点的动画持续时间(占总进度的比例)
|
const fromPositions = new Float32Array(n * 3);
|
||||||
const delayPerPoint = 0.6 / n; // 剩余0.6进度用于错峰
|
const toPositions = new Float32Array(n * 3);
|
||||||
|
const delays = new Float32Array(n);
|
||||||
|
const moveDuration = 0.4;
|
||||||
|
const delayPerPoint = n > 0 ? 0.6 / n : 0;
|
||||||
|
const fadeInDuration = 0.3;
|
||||||
|
|
||||||
// 为CS7.5的点分配起始位置(从CS7和CS8的实际数据中获取)
|
const sourceLeftByType = new Map<string, Point[]>();
|
||||||
// 基于细胞类型或颜色来分配起始位置,确保相同颜色的点从两侧出现
|
sourceLeftData.forEach(point => {
|
||||||
// Left/right source point placeholders removed (unused)
|
|
||||||
|
|
||||||
// 按细胞类型分组CS7和CS8数据
|
|
||||||
const cs7ByType = new Map<string, Point[]>();
|
|
||||||
const cs8ByType = new Map<string, Point[]>();
|
|
||||||
|
|
||||||
cs7Data.forEach(point => {
|
|
||||||
const type = point.value as string;
|
const type = point.value as string;
|
||||||
if (!cs7ByType.has(type)) cs7ByType.set(type, []);
|
if (!sourceLeftByType.has(type)) sourceLeftByType.set(type, []);
|
||||||
cs7ByType.get(type)!.push(point);
|
sourceLeftByType.get(type)!.push(point);
|
||||||
});
|
});
|
||||||
|
|
||||||
cs8Data.forEach(point => {
|
const sourceRightByType = new Map<string, Point[]>();
|
||||||
|
sourceRightData.forEach(point => {
|
||||||
const type = point.value as string;
|
const type = point.value as string;
|
||||||
if (!cs8ByType.has(type)) cs8ByType.set(type, []);
|
if (!sourceRightByType.has(type)) sourceRightByType.set(type, []);
|
||||||
cs8ByType.get(type)!.push(point);
|
sourceRightByType.get(type)!.push(point);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const to = cs75Data[i];
|
const to = targetData[i];
|
||||||
|
if (i < 5) console.log(`Target Point ${i}: to (${to.x.toFixed(2)}, ${to.y.toFixed(2)}, ${to.z.toFixed(2)})`);
|
||||||
const cellType = to.value as string;
|
const cellType = to.value as string;
|
||||||
let fromX, fromY, fromZ;
|
|
||||||
|
|
||||||
// 基于细胞类型和索引的奇偶性来决定从哪一侧开始
|
|
||||||
const shouldStartFromLeft = i % 2 === 0;
|
const shouldStartFromLeft = i % 2 === 0;
|
||||||
|
let sourcePoint: Point;
|
||||||
if (shouldStartFromLeft) {
|
if (shouldStartFromLeft) {
|
||||||
// 从左侧CS7数据开始
|
const pointsOfType = sourceLeftByType.get(cellType) || [];
|
||||||
const cs7PointsOfType = cs7ByType.get(cellType) || [];
|
if (pointsOfType.length > 0) {
|
||||||
const sourceIndex = Math.floor(i / 2) % cs7PointsOfType.length;
|
const sourceIndex = Math.floor(i / 2) % pointsOfType.length;
|
||||||
const sourcePoint = cs7PointsOfType.length > 0
|
sourcePoint = pointsOfType[sourceIndex];
|
||||||
? cs7PointsOfType[sourceIndex]
|
} else {
|
||||||
: cs7Data[i % cs7Data.length];
|
sourcePoint = pickFallbackPoint(sourceLeftData, sourceRightData, i, to);
|
||||||
fromX = sourcePoint.x - 0.5;
|
}
|
||||||
fromY = sourcePoint.y;
|
|
||||||
fromZ = sourcePoint.z;
|
|
||||||
} else {
|
} else {
|
||||||
// 从右侧CS8数据开始
|
const pointsOfType = sourceRightByType.get(cellType) || [];
|
||||||
const cs8PointsOfType = cs8ByType.get(cellType) || [];
|
if (pointsOfType.length > 0) {
|
||||||
const sourceIndex = Math.floor(i / 2) % cs8PointsOfType.length;
|
const sourceIndex = Math.floor(i / 2) % pointsOfType.length;
|
||||||
const sourcePoint = cs8PointsOfType.length > 0
|
sourcePoint = pointsOfType[sourceIndex];
|
||||||
? cs8PointsOfType[sourceIndex]
|
} else {
|
||||||
: cs8Data[i % cs8Data.length];
|
sourcePoint = pickFallbackPoint(sourceRightData, sourceLeftData, i, to);
|
||||||
fromX = sourcePoint.x + 0.5;
|
}
|
||||||
fromY = sourcePoint.y;
|
|
||||||
fromZ = sourcePoint.z;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加一些随机延迟,使动画更自然
|
const offsetX = shouldStartFromLeft ? leftOffsetX : rightOffsetX;
|
||||||
|
const fromBaseIndex = i * 3;
|
||||||
|
const fromX = sourcePoint.x + offsetX;
|
||||||
|
const fromY = sourcePoint.y;
|
||||||
|
const fromZ = sourcePoint.z;
|
||||||
|
const toBaseX = to.x + targetOffsetX;
|
||||||
|
const toBaseY = to.y;
|
||||||
|
const toBaseZ = to.z;
|
||||||
|
|
||||||
|
if (i < 5) {
|
||||||
|
console.log(`Point ${i}: from (${fromX.toFixed(2)}, ${fromY.toFixed(2)}, ${fromZ.toFixed(2)}) to (${toBaseX.toFixed(2)}, ${toBaseY.toFixed(2)}, ${toBaseZ.toFixed(2)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fromPositions[fromBaseIndex] = fromX;
|
||||||
|
fromPositions[fromBaseIndex + 1] = fromY;
|
||||||
|
fromPositions[fromBaseIndex + 2] = fromZ;
|
||||||
|
toPositions[fromBaseIndex] = toBaseX;
|
||||||
|
toPositions[fromBaseIndex + 1] = toBaseY;
|
||||||
|
toPositions[fromBaseIndex + 2] = toBaseZ;
|
||||||
|
|
||||||
|
positionsArray[fromBaseIndex] = fromX;
|
||||||
|
positionsArray[fromBaseIndex + 1] = fromY;
|
||||||
|
positionsArray[fromBaseIndex + 2] = fromZ;
|
||||||
|
|
||||||
|
const color = colorMap.get(cellType) || new THREE.Color('#CCCCCC');
|
||||||
|
colorsArray[fromBaseIndex] = color.r;
|
||||||
|
colorsArray[fromBaseIndex + 1] = color.g;
|
||||||
|
colorsArray[fromBaseIndex + 2] = color.b;
|
||||||
|
|
||||||
|
alphasArray[i] = 0;
|
||||||
|
|
||||||
|
recordPosition(fromX, fromY, fromZ);
|
||||||
|
recordPosition(toBaseX, toBaseY, toBaseZ);
|
||||||
|
|
||||||
const baseDelay = i * delayPerPoint;
|
const baseDelay = i * delayPerPoint;
|
||||||
const randomDelay = (Math.random() - 0.5) * 0.1; // 添加±0.05的随机延迟
|
const randomDelay = (Math.random() - 0.5) * 0.1;
|
||||||
const delay = baseDelay + randomDelay;
|
delays[i] = baseDelay + randomDelay;
|
||||||
|
|
||||||
let pointProgress = (animationProgress - delay) / moveDuration;
|
|
||||||
pointProgress = Math.max(0, Math.min(1, pointProgress));
|
|
||||||
|
|
||||||
// 使用缓动函数使动画更平滑
|
|
||||||
const easeProgress = pointProgress < 0.5
|
|
||||||
? 2 * pointProgress * pointProgress
|
|
||||||
: 1 - Math.pow(-2 * pointProgress + 2, 2) / 2;
|
|
||||||
|
|
||||||
const x = fromX + (to.x - fromX) * easeProgress;
|
|
||||||
const y = fromY + (to.y - fromY) * easeProgress;
|
|
||||||
const z = fromZ + (to.z - fromZ) * easeProgress;
|
|
||||||
|
|
||||||
// 计算透明度:从透明(0)到不透明(1)
|
|
||||||
// 透明度在动画的前30%时间内从0过渡到1
|
|
||||||
const fadeInDuration = 0.3;
|
|
||||||
const fadeProgress = Math.min(pointProgress / fadeInDuration, 1);
|
|
||||||
const alpha = fadeProgress;
|
|
||||||
|
|
||||||
positions.push(x, y, z);
|
|
||||||
const c = colorMap.get(to.value as string) || new THREE.Color('#CCCCCC');
|
|
||||||
colors.push(c.r, c.g, c.b);
|
|
||||||
alphas.push(alpha);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positionsArray, 3));
|
||||||
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
|
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colorsArray, 3));
|
||||||
geometry.setAttribute("alpha", new THREE.Float32BufferAttribute(alphas, 1));
|
geometry.setAttribute("alpha", new THREE.Float32BufferAttribute(alphasArray, 1));
|
||||||
|
|
||||||
// 创建自定义着色器材质来处理透明度
|
|
||||||
const vertexShader = `
|
const vertexShader = `
|
||||||
attribute float alpha;
|
attribute float alpha;
|
||||||
attribute vec3 color;
|
attribute vec3 color;
|
||||||
@ -475,59 +611,94 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
|
|||||||
varying float vAlpha;
|
varying float vAlpha;
|
||||||
varying vec3 vColor;
|
varying vec3 vColor;
|
||||||
void main() {
|
void main() {
|
||||||
// 使用纹理贴图
|
|
||||||
vec4 texColor = texture2D(pointTexture, gl_PointCoord);
|
vec4 texColor = texture2D(pointTexture, gl_PointCoord);
|
||||||
|
|
||||||
// 计算当前片段在点内的相对位置
|
|
||||||
vec2 center = gl_PointCoord - vec2(0.5);
|
vec2 center = gl_PointCoord - vec2(0.5);
|
||||||
float dist = length(center);
|
float dist = length(center);
|
||||||
|
|
||||||
// 创建圆形遮罩,距离中心0.5半径内的片段可见
|
|
||||||
if (dist > 0.5) {
|
if (dist > 0.5) {
|
||||||
discard; // 丢弃圆形外的片段
|
discard;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将纹理颜色与顶点颜色混合
|
|
||||||
gl_FragColor = vec4(vColor * texColor.rgb, vAlpha * texColor.a);
|
gl_FragColor = vec4(vColor * texColor.rgb, vAlpha * texColor.a);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const texture = createTexture(); // 创建纹理贴图(与PointsMaterial相同)
|
|
||||||
|
|
||||||
const material = new THREE.ShaderMaterial({
|
const material = new THREE.ShaderMaterial({
|
||||||
vertexShader: vertexShader,
|
vertexShader,
|
||||||
fragmentShader: fragmentShader,
|
fragmentShader,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
|
|
||||||
blending: THREE.NormalBlending,
|
blending: THREE.NormalBlending,
|
||||||
uniforms: {
|
uniforms: {
|
||||||
pointTexture: { value: texture }
|
pointTexture: { value: pointTexture }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const points = new THREE.Points(geometry, material);
|
const points = new THREE.Points(geometry, material);
|
||||||
scene.add(points);
|
scene.add(points);
|
||||||
|
disposableGeometries.push(geometry);
|
||||||
|
disposableMaterials.push(material);
|
||||||
|
|
||||||
|
animationStateRef.current = {
|
||||||
|
positionsAttr: geometry.getAttribute("position") as THREE.BufferAttribute,
|
||||||
|
alphaAttr: geometry.getAttribute("alpha") as THREE.BufferAttribute,
|
||||||
|
fromPositions,
|
||||||
|
toPositions,
|
||||||
|
delays,
|
||||||
|
moveDuration,
|
||||||
|
fadeInDuration
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
animationStateRef.current = null;
|
||||||
}
|
}
|
||||||
// 渲染循环
|
|
||||||
|
if (allPositions.length > 0) {
|
||||||
|
const posArr = new Float32Array(allPositions);
|
||||||
|
const box = new THREE.Box3().setFromArray(posArr);
|
||||||
|
const center = new THREE.Vector3();
|
||||||
|
box.getCenter(center);
|
||||||
|
const size = new THREE.Vector3();
|
||||||
|
box.getSize(size);
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
const fitScale = maxDim === 0 ? 1 : 32 / maxDim;
|
||||||
|
scene.position.set(-center.x, -center.y, -center.z);
|
||||||
|
scene.scale.set(fitScale, fitScale, fitScale);
|
||||||
|
}
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
animationIdRef.current = requestAnimationFrame(animate);
|
animationIdRef.current = requestAnimationFrame(animate);
|
||||||
controls.update();
|
controls.update();
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
};
|
};
|
||||||
animate();
|
animate();
|
||||||
// 清理
|
|
||||||
|
applyAnimationProgress(animationProgress);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (animationIdRef.current !== null) {
|
if (animationIdRef.current !== null) {
|
||||||
cancelAnimationFrame(animationIdRef.current);
|
cancelAnimationFrame(animationIdRef.current);
|
||||||
|
animationIdRef.current = null;
|
||||||
}
|
}
|
||||||
if (controlsRef.current) {
|
if (controlsRef.current) {
|
||||||
controlsRef.current.dispose();
|
controlsRef.current.dispose();
|
||||||
|
controlsRef.current = null;
|
||||||
}
|
}
|
||||||
if (rendererRef.current) {
|
if (rendererRef.current) {
|
||||||
rendererRef.current.dispose();
|
rendererRef.current.dispose();
|
||||||
|
rendererRef.current = null;
|
||||||
}
|
}
|
||||||
|
disposableMaterials.forEach((material) => material.dispose());
|
||||||
|
disposableGeometries.forEach((geometry) => geometry.dispose());
|
||||||
|
disposableTextures.forEach((texture) => texture.dispose());
|
||||||
|
if (containerRef.current) {
|
||||||
|
while (containerRef.current.firstChild) {
|
||||||
|
containerRef.current.removeChild(containerRef.current.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animationStateRef.current = null;
|
||||||
};
|
};
|
||||||
}, [cs7Data, cs8Data, cs75Data, animationProgress]);
|
}, [sourceLeftData, sourceRightData, targetData, leftOffsetX, rightOffsetX, targetOffsetX, applyAnimationProgress]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyAnimationProgress(animationProgress);
|
||||||
|
}, [animationProgress, applyAnimationProgress]);
|
||||||
|
|
||||||
return <div ref={containerRef} className="point-cloud-container" style={{ width: "100%", height: "700px", minHeight: 600 }} />;
|
return <div ref={containerRef} className="point-cloud-container" style={{ width: "100%", height: "700px", minHeight: 600 }} />;
|
||||||
};
|
};
|
||||||
@ -551,5 +722,4 @@ const createTexture = () => {
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
return new THREE.CanvasTexture(canvas);
|
return new THREE.CanvasTexture(canvas);
|
||||||
};
|
};
|
||||||
|
export default EmbryoGeneration;
|
||||||
export default EmbryoGeneration;
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user