Skip to content

Commit 5d846d0

Browse files
authored
Merge pull request #454 from EarthyScience/jp/animation_update
Jp/animation update
2 parents ea6b96b + df5d3c0 commit 5d846d0

15 files changed

+540
-184
lines changed

src/components/plots/CountryBorders.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,14 @@ const CountryBorders = () => {
233233
const isFlatMap = plotType == "flat"
234234
const timeRatio = Math.max(dataShape[0]/dataShape[2],2)
235235
const depthScale = timeRatio*timeScale
236+
const aspectRatio = dataShape[2]/dataShape[1]
236237

237238
const globalScale = isPC ? dataShape[2]/500 : 1
238239

239240
return(
240241
<group
241242
rotation={[rotateFlat ? -Math.PI/2 : 0, 0, 0]}
242-
scale={[globalScale, globalScale, globalScale]}
243+
scale={[globalScale, globalScale * (spherize ? 1 : aspectRatio), globalScale]}
243244
>
244245
<group
245246
visible={showBorders && !(analysisMode && axis != 0)}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"use client";
2+
import { useGlobalStore, useImageExportStore, usePlotStore } from '@/utils/GlobalStates'
3+
import { invalidate, useThree } from '@react-three/fiber';
4+
import React, { useEffect, useRef } from 'react'
5+
import { useShallow } from 'zustand/shallow'
6+
import * as THREE from 'three'
7+
import { lerp } from 'three/src/math/MathUtils.js';
8+
9+
10+
export const KeyFramePreviewer = () => {
11+
const {keyFrames, currentFrame, previewKeyFrames, frames,
12+
frameRate, useTime, timeRate, orbit, loopTime,} = useImageExportStore(useShallow(state => ({
13+
keyFrames:state.keyFrames, currentFrame:state.currentFrame, previewKeyFrames:state.previewKeyFrames,
14+
frames:state.frames, frameRate:state.frameRate, useTime:state.useTime, timeRate:state.timeRate,
15+
orbit:state.orbit, loopTime:state.loopTime
16+
})))
17+
18+
const {camera} = useThree();
19+
const isAnimating = useRef(false)
20+
const originalAnimProg = useRef<number>(0)
21+
22+
const KeyFrameLerper = (startState: Record<string,any>, endState:Record<string,any>, alpha:number, useCamera=true) => {
23+
const startVizState = startState["visual"]
24+
const endVizState = endState["visual"]
25+
const lerpedVizState: Record<string, any> = {};
26+
const lerpedCamState: Record<string, any> = {};
27+
Object.keys(startVizState).forEach(key => {
28+
const sourceValue = startVizState[key];
29+
const targetValue = endVizState[key];
30+
31+
// Check if both values are numbers
32+
if (typeof sourceValue === 'number' && typeof targetValue === 'number') {
33+
lerpedVizState[key] = lerp(sourceValue, targetValue, alpha);
34+
}
35+
else if (sourceValue.length){ // If Array
36+
lerpedVizState[key] = []
37+
for (let i = 0; i < sourceValue.length; i++){
38+
lerpedVizState[key][i] = lerp(sourceValue[i], targetValue[i], alpha);
39+
}
40+
}
41+
// Handle Vector3, arrays, or other lerpable objects
42+
else if (sourceValue?.lerp && typeof sourceValue.lerp === 'function') {
43+
lerpedVizState[key] = sourceValue.clone().lerp(targetValue, alpha);
44+
}
45+
// For non-numeric values, just copy from target
46+
else {
47+
lerpedVizState[key] = targetValue;
48+
}
49+
});
50+
usePlotStore.setState(lerpedVizState)
51+
52+
if (useCamera){ // Don't lerp camera if orbiting
53+
const startCamState = startState["camera"]
54+
const endCamState = endState["camera"]
55+
Object.keys(startCamState).forEach(key => {
56+
const sourceValue = startCamState[key];
57+
const targetValue = endCamState[key];
58+
if (sourceValue.isEuler){
59+
const startQuat = new THREE.Quaternion().setFromEuler(sourceValue);
60+
const endQuat = new THREE.Quaternion().setFromEuler(targetValue);
61+
62+
const resultQuat = new THREE.Quaternion().copy(startQuat).slerp(endQuat, alpha);
63+
lerpedCamState[key]= new THREE.Euler().setFromQuaternion(resultQuat);
64+
65+
} else{
66+
lerpedCamState[key] = sourceValue.clone().lerp(targetValue,alpha)
67+
}
68+
});
69+
camera.position.copy(lerpedCamState.position)
70+
camera.rotation.copy(lerpedCamState.rotation)
71+
camera.updateProjectionMatrix();
72+
invalidate();
73+
}
74+
}
75+
76+
77+
// ----- PREVIEW FUNCTIONS ---- //
78+
79+
// PREVIEW KEYFRAME
80+
useEffect(()=>{
81+
if (!keyFrames || isAnimating.current) return;
82+
const keyFrameList = Array.from(keyFrames.keys()).sort((a, b) => a - b)
83+
const keyFrameIdx = keyFrameList.findLastIndex(n => n <= currentFrame)
84+
const startFrame = keyFrameList[keyFrameIdx]
85+
const origCamera = keyFrames.get(keyFrameList[0]).camera
86+
const originalPos = {
87+
x: origCamera.position.x,
88+
y: origCamera.position.y,
89+
z: origCamera.position.z
90+
};
91+
const radius = Math.sqrt(originalPos.x ** 2 + originalPos.z ** 2);
92+
const originalAngle = Math.atan2(originalPos.x, originalPos.z);
93+
if (keyFrameIdx+1 < keyFrameList.length){
94+
const endFrame = keyFrameList[keyFrameIdx+1]
95+
const thisFrames = endFrame-startFrame;
96+
const alpha = Math.max(currentFrame-startFrame, 0)/thisFrames;
97+
const startState = keyFrames?.get(startFrame)
98+
const endState = keyFrames?.get(endFrame)
99+
KeyFrameLerper(startState, endState, alpha, !orbit)
100+
} else {
101+
const {visual, camera:cameraState} = keyFrames.get(startFrame)
102+
usePlotStore.setState(visual)
103+
if (!orbit){
104+
camera.position.copy(cameraState.position)
105+
camera.rotation.copy(cameraState.rotation)
106+
camera.updateProjectionMatrix();
107+
invalidate();
108+
}
109+
}
110+
if (orbit){
111+
const angle = (currentFrame / (frames+1)) * Math.PI * 2;
112+
const newAngle = originalAngle + angle;
113+
camera.position.x = radius * Math.sin(newAngle);
114+
camera.position.z = radius * Math.cos(newAngle);
115+
camera.lookAt(0, 0, 0);
116+
camera.updateProjectionMatrix();
117+
invalidate();
118+
}
119+
},[currentFrame])
120+
121+
// PREVIEW ANIMATION
122+
useEffect(()=>{
123+
if (!keyFrames || isAnimating.current) return;
124+
const {animProg, setAnimProg} = usePlotStore.getState()
125+
originalAnimProg.current = animProg
126+
const keyFrameList = Array.from(keyFrames.keys()).sort((a, b) => a - b)
127+
const timeRatio = timeRate/frameRate;
128+
const {dataShape} = useGlobalStore.getState()
129+
const timeFrames = dataShape[dataShape.length-3]
130+
const dt = 1/timeFrames
131+
132+
let keyFrameIdx = 0;
133+
let frame = 0;
134+
135+
const origCamera = keyFrames.get(keyFrameList[0]).camera
136+
const originalPos = {
137+
x: origCamera.position.x,
138+
y: origCamera.position.y,
139+
z: origCamera.position.z
140+
};
141+
const radius = Math.sqrt(originalPos.x ** 2 + originalPos.z ** 2);
142+
const originalAngle = Math.atan2(originalPos.x, originalPos.z);
143+
144+
const intervalId = setInterval(()=>{
145+
if (frame >= frames) {
146+
clearInterval(intervalId);
147+
isAnimating.current = false;
148+
setAnimProg(originalAnimProg.current)
149+
return;
150+
}
151+
useImageExportStore.getState().setCurrentFrame(frame)
152+
const startFrame = keyFrameList[keyFrameIdx];
153+
if (keyFrameIdx + 1 < keyFrameList.length) {
154+
if (useTime) {
155+
let newProg = dt * Math.floor(frame * timeRatio) + animProg;
156+
newProg = loopTime ? newProg - Math.floor(newProg) : Math.min(newProg, 1);
157+
setAnimProg(newProg);
158+
}
159+
const endFrame = keyFrameList[keyFrameIdx + 1];
160+
const thisFrames = endFrame - startFrame;
161+
const alpha = Math.max(frame - startFrame, 0) / thisFrames;
162+
const startState = keyFrames.get(startFrame);
163+
const endState = keyFrames.get(endFrame);
164+
KeyFrameLerper(startState, endState, alpha, !orbit); // Don't lerp camera if orbiting
165+
if (frame >= endFrame) keyFrameIdx++;
166+
} else {
167+
const { visual, camera: cameraState } = keyFrames.get(startFrame);
168+
usePlotStore.setState(visual);
169+
if (!orbit){ // Don't lerp camera if orbiting
170+
camera.position.copy(cameraState.position);
171+
camera.rotation.copy(cameraState.rotation);
172+
camera.updateProjectionMatrix();
173+
invalidate();
174+
}
175+
}
176+
if (orbit){
177+
const angle = (frame / (frames+1)) * Math.PI * 2;
178+
const newAngle = originalAngle + angle;
179+
camera.position.x = radius * Math.sin(newAngle);
180+
camera.position.z = radius * Math.cos(newAngle);
181+
camera.lookAt(0, 0, 0);
182+
camera.updateProjectionMatrix();
183+
invalidate();
184+
}
185+
frame ++;
186+
}, 1000/frameRate)
187+
188+
isAnimating.current= true;
189+
190+
return () => clearInterval(intervalId);
191+
192+
},[previewKeyFrames])
193+
return null
194+
}
195+
196+

src/components/plots/Plot.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { OrbitControls } from '@react-three/drei';
22
import React, { useMemo, useRef, useState, useEffect } from 'react';
33
import * as THREE from 'three';
4-
import { PointCloud, UVCube, DataCube, FlatMap, Sphere, CountryBorders, AxisLines, SphereBlocks, FlatBlocks } from '@/components/plots';
4+
import { PointCloud, UVCube, DataCube, FlatMap, Sphere, CountryBorders, AxisLines, SphereBlocks, FlatBlocks, KeyFramePreviewer } from '@/components/plots';
55
import { Canvas, invalidate, useThree } from '@react-three/fiber';
66
import { ArrayToTexture, CreateTexture } from '@/components/textures';
77
import { ZarrDataset } from '../zarr/ZarrLoaderLRU';
8-
import { useAnalysisStore, useGlobalStore, usePlotStore, useZarrStore } from '@/utils/GlobalStates';
8+
import { useAnalysisStore, useGlobalStore, useImageExportStore, usePlotStore, useZarrStore } from '@/utils/GlobalStates';
99
import { useShallow } from 'zustand/shallow';
1010
import { Navbar, Colorbar } from '../ui';
1111
import AnalysisInfo from './AnalysisInfo';
@@ -33,10 +33,11 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{
3333
useOrtho: state.useOrtho,
3434
displaceSurface: state.displaceSurface
3535
})))
36+
const {setCameraRef} = useImageExportStore(useShallow(state=>({setCameraRef:state.setCameraRef})))
3637
const orbitRef = useRef<OrbitControlsImpl | null>(null)
3738
const hasMounted = useRef(false);
39+
const cameraRef = useRef<THREE.Camera | null>(null)
3840
const {set, camera, size} = useThree()
39-
4041
// Reset Camera Position and Target
4142
useEffect(()=>{
4243
if (!hasMounted.current) {
@@ -106,6 +107,8 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{
106107
newCamera.position.copy(camera.position.normalize().multiply(new THREE.Vector3(4, 4, 4))) // 4 seems like good distance
107108
newCamera.rotation.copy(camera.rotation)
108109
}
110+
cameraRef.current = newCamera
111+
setCameraRef(cameraRef)
109112
set({ camera: newCamera})
110113
if (orbitRef.current) {
111114
orbitRef.current.object = newCamera
@@ -325,6 +328,7 @@ const Plot = ({ZarrDS}:{ZarrDS: ZarrDataset}) => {
325328
gl={{ preserveDrawingBuffer: true }}
326329
dpr={[DPR,DPR]}
327330
>
331+
<KeyFramePreviewer/>
328332
<CountryBorders/>
329333
<ExportCanvas show={show}/>
330334
{show && <AxisLines />}
@@ -344,8 +348,8 @@ const Plot = ({ZarrDS}:{ZarrDS: ZarrDataset}) => {
344348
{displaceSurface ? <Sphere textures={textures} ZarrDS={ZarrDS} /> : <SphereBlocks textures={textures} />}
345349
</>
346350
}
347-
<Orbiter isFlat={(isFlat || (!isFlat && plotType == "flat"))} />
348-
{(isFlat || (!isFlat && plotType == "flat")) && <>
351+
<Orbiter isFlat={plotType == "flat"} />
352+
{plotType == "flat" && show && <>
349353
{displaceSurface && <FlatMap textures={textures as THREE.DataTexture | THREE.Data3DTexture[]} infoSetters={infoSetters} ZarrDS={ZarrDS}/> }
350354
{!displaceSurface && <FlatBlocks textures={textures} />}
351355
</>

src/components/plots/Sphere.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,11 @@ export const Sphere = ({textures, ZarrDS} : {textures: THREE.Data3DTexture[] | T
108108
setBoundsObj(prev=>{ return {...newBoundObj, ...prev}})
109109
}
110110

111-
112111
const [lonBounds, latBounds] = useMemo(()=>{ //The bounds for the shader. It takes the middle point of the furthest coordinate and adds the distance to edge of pixel
113112
const newLatStep = latResolution/2;
114113
const newLonStep = lonResolution/2;
115-
const newLonBounds = [Math.max(lonExtent[0]-newLonStep, -180), Math.min(lonExtent[1]+newLonStep, 180)]
116-
const newLatBounds = [Math.max(latExtent[0]-newLatStep, -90), Math.min(latExtent[1]+newLatStep, 90)]
114+
const newLonBounds = [lonExtent[0]-newLonStep, lonExtent[1]+newLonStep]
115+
const newLatBounds = [latExtent[0]-newLatStep, latExtent[1]+newLatStep]
117116
return [newLonBounds, newLatBounds]
118117
},[latExtent, lonExtent, lonResolution, latResolution])
119118

src/components/plots/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export {CountryBorders} from './CountryBorders'
1212
export {AxisLines} from './AxisLines'
1313
export {LandingShapes} from './MorphingPoints'
1414
export {SphereBlocks} from './SphereBlocks'
15-
export {FlatBlocks} from './FlatBlocks'
15+
export {FlatBlocks} from './FlatBlocks'
16+
export {KeyFramePreviewer} from './KeyFramePreviewer'

src/components/textures/shaders/sphereBlocksVert.glsl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ uniform float animateProg;
1515

1616
vec3 givePosition(vec2 uv) {
1717
// Reverse the normalization using the bounds
18-
float longitude = (1.0 - uv.x) * (lonBounds.y - lonBounds.x) + lonBounds.x;
18+
float longitude = -((1.0 - uv.x) * (lonBounds.y - lonBounds.x) + lonBounds.x);
1919
float latitude = uv.y * (latBounds.y - latBounds.x) + latBounds.x;
2020

2121
// Convert to Cartesian coordinates
@@ -25,7 +25,7 @@ vec3 givePosition(vec2 uv) {
2525

2626
return vec3(x, y, z);
2727
}
28-
28+
2929

3030
float sample1(vec3 p, int index) { // Shader doesn't support dynamic indexing so we gotta use switching
3131
if (index == 0) return texture(map[0], p).r;

src/components/textures/shaders/sphereBlocksVertFlat.glsl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ uniform float animateProg;
1515

1616
vec3 givePosition(vec2 uv) {
1717
// Reverse the normalization using the bounds
18-
float longitude = (1.0 - uv.x) * (lonBounds.y - lonBounds.x) + lonBounds.x;
18+
float longitude = -((1.0 - uv.x) * (lonBounds.y - lonBounds.x) + lonBounds.x);
1919
float latitude = uv.y * (latBounds.y - latBounds.x) + latBounds.x;
2020

2121
// Convert to Cartesian coordinates

src/components/textures/shaders/sphereFrag.glsl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ uniform float nanAlpha;
2626
vec2 giveUV(vec3 position){
2727
vec3 n = normalize(position);
2828
float latitude = asin(n.y);
29-
float longitude = atan(n.z, n.x);
29+
float longitude = -atan(n.z, n.x);
30+
3031
latitude = (latitude - latBounds.x)/(latBounds.y - latBounds.x);
3132
longitude = (longitude - lonBounds.x)/(lonBounds.y - lonBounds.x);
3233

@@ -92,7 +93,7 @@ void main(){
9293
color.rgb *= cond ? 1. : 0.65;
9394
}
9495
} else {
95-
color = vec4(nanColor, 1.); // Black
96+
color = vec4(nanColor, 1.);
9697
color.a = nanAlpha;
9798
}
9899

src/components/textures/shaders/sphereVertex.glsl

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ uniform float animateProg;
1212
vec2 giveUV(vec3 position){
1313
vec3 n = normalize(position);
1414
float latitude = asin(n.y);
15-
float longitude = atan(n.z, n.x);
15+
float longitude = -atan(n.z, n.x);
1616
latitude = (latitude - latBounds.x)/(latBounds.y - latBounds.x);
1717
longitude = (longitude - lonBounds.x)/(lonBounds.y - lonBounds.x);
1818

@@ -41,19 +41,28 @@ out vec3 aPosition;
4141

4242
void main() {
4343
vec2 uv = giveUV(position); // We can't just pass this as a varying because the fragment will try to interpoalte between the seems which looks bad
44-
vec3 normal = normalize(position);
45-
int zStepSize = int(textureDepths.y) * int(textureDepths.x);
46-
int yStepSize = int(textureDepths.x);
47-
vec3 texCoord = vec3(uv, animateProg);
48-
ivec3 idx = clamp(ivec3(texCoord * textureDepths), ivec3(0), ivec3(textureDepths) - 1); // Ivec3 is like running a "floor" operation on all three at once. The clamp is because the very last idx is OOR
49-
int textureIdx = idx.z * zStepSize + idx.y * yStepSize + idx.x;
50-
vec3 localCoord = texCoord * textureDepths; // Scale up
51-
localCoord = fract(localCoord);
52-
53-
float dispStrength = sample1(localCoord, textureIdx);
54-
float noNan = float(dispStrength != 1.0);
55-
vec3 newPos = position + (normal * (dispStrength-displaceZero) * noNan * displacement);
56-
aPosition = position; //Pass out position for sphere frag
57-
vec4 worldPos = modelViewMatrix * vec4( newPos, 1.0 );
58-
gl_Position = projectionMatrix * worldPos;
44+
bool inBounds = all(greaterThanEqual(uv, vec2(0.0))) &&
45+
all(lessThanEqual(uv, vec2(1.0)));
46+
aPosition = position;
47+
if (inBounds){
48+
vec3 normal = normalize(position);
49+
int zStepSize = int(textureDepths.y) * int(textureDepths.x);
50+
int yStepSize = int(textureDepths.x);
51+
vec3 texCoord = vec3(uv, animateProg);
52+
ivec3 idx = clamp(ivec3(texCoord * textureDepths), ivec3(0), ivec3(textureDepths) - 1); // Ivec3 is like running a "floor" operation on all three at once. The clamp is because the very last idx is OOR
53+
int textureIdx = idx.z * zStepSize + idx.y * yStepSize + idx.x;
54+
vec3 localCoord = texCoord * textureDepths; // Scale up
55+
localCoord = fract(localCoord);
56+
float dispStrength = sample1(localCoord, textureIdx);
57+
float noNan = float(dispStrength != 1.0);
58+
vec3 newPos = position + (normal * (dispStrength-displaceZero) * noNan * displacement);
59+
//Pass out position for sphere frag
60+
vec4 worldPos = modelViewMatrix * vec4( newPos, 1.0 );
61+
gl_Position = projectionMatrix * worldPos;
62+
} else {
63+
vec4 worldPos = modelViewMatrix * vec4( position, 1.0 );
64+
gl_Position = projectionMatrix * worldPos;
65+
}
66+
67+
5968
}

0 commit comments

Comments
 (0)