Skip to content

Commit 6b7fa6a

Browse files
authored
Merge pull request #457 from EarthyScience/jp/keyframe-editor
Added keyframe editor and export preview overlay
2 parents 80a9980 + 9870db3 commit 6b7fa6a

File tree

9 files changed

+233
-101
lines changed

9 files changed

+233
-101
lines changed

src/components/plots/KeyFramePreviewer.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ export const KeyFramePreviewer = () => {
1919
const isAnimating = useRef(false)
2020
const intervalRef = useRef<NodeJS.Timeout | null>(null)
2121
const originalAnimProg = useRef<number>(0)
22-
const {originalPos, originalAngle, radius } = useMemo(()=>{
22+
const { originalAngle, radius } = useMemo(()=>{
23+
isAnimating.current = false; // End animation if Keyframes change
24+
clearTimeout(intervalRef.current as NodeJS.Timeout)
2325
if (!keyFrames){
24-
isAnimating.current = false; // End animation if Keyframes change
25-
clearTimeout(intervalRef.current as NodeJS.Timeout)
26-
return {originalPos:new THREE.Vector3(), radius:0, originalAngle:0}
26+
return { radius:0, originalAngle:0}
2727
};
2828
const keyFrameList = Array.from(keyFrames.keys()).sort((a, b) => a - b)
29+
if(keyFrameList.length === 0){
30+
return { radius:0, originalAngle:0}
31+
}
2932
const origCamera = keyFrames.get(keyFrameList[0]).camera
3033
const originalPos = {
3134
x: origCamera.position.x,
@@ -34,9 +37,7 @@ export const KeyFramePreviewer = () => {
3437
};
3538
const radius = Math.sqrt(originalPos.x ** 2 + originalPos.z ** 2);
3639
const originalAngle = Math.atan2(originalPos.x, originalPos.z);
37-
isAnimating.current = false; // End animation if Keyframes change
38-
clearTimeout(intervalRef.current as NodeJS.Timeout)
39-
return {originalPos, radius, originalAngle}
40+
return { radius, originalAngle}
4041
},[keyFrames])
4142

4243
const KeyFrameLerper = (startState: Record<string,any>, endState:Record<string,any>, alpha:number, useCamera=true) => {
@@ -93,14 +94,14 @@ export const KeyFramePreviewer = () => {
9394
}
9495
}
9596

96-
9797
// ----- PREVIEW FUNCTIONS ---- //
9898

9999
// PREVIEW KEYFRAME
100100
useEffect(()=>{
101101
if (!keyFrames || isAnimating.current) return;
102102
const keyFrameList = Array.from(keyFrames.keys()).sort((a, b) => a - b)
103-
const keyFrameIdx = keyFrameList.findLastIndex(n => n <= currentFrame)
103+
if (keyFrameList.length == 0) return;
104+
const keyFrameIdx = Math.max(keyFrameList.findLastIndex(n => n <= currentFrame), 0)
104105
const startFrame = keyFrameList[keyFrameIdx]
105106
if (keyFrameIdx+1 < keyFrameList.length){
106107
const endFrame = keyFrameList[keyFrameIdx+1]
@@ -144,7 +145,7 @@ export const KeyFramePreviewer = () => {
144145
let keyFrameIdx = 0;
145146
let frame = 0;
146147
intervalRef.current = setInterval(()=>{
147-
if (frame >= frames) {
148+
if (frame > frames) {
148149
clearInterval(intervalRef.current as NodeJS.Timeout);
149150
isAnimating.current = false;
150151
setAnimProg(originalAnimProg.current)

src/components/plots/Plot.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import { ArrayToTexture, CreateTexture } from '@/components/textures';
77
import { ZarrDataset } from '../zarr/ZarrLoaderLRU';
88
import { useAnalysisStore, useGlobalStore, useImageExportStore, usePlotStore, useZarrStore } from '@/utils/GlobalStates';
99
import { useShallow } from 'zustand/shallow';
10-
import { Navbar, Colorbar } from '../ui';
10+
import { Navbar, Colorbar, ExportExtent } from '../ui';
1111
import AnalysisInfo from './AnalysisInfo';
1212
import { OrbitControls as OrbitControlsImpl } from 'three-stdlib';
1313
import AnalysisWG from './AnalysisWG';
1414
import { ParseExtent } from '@/utils/HelperFuncs';
1515
import ExportCanvas from '@/utils/ExportCanvas';
16+
import KeyFrames from '../ui/KeyFrames';
1617

1718

1819
const TransectNotice = () =>{
@@ -155,7 +156,7 @@ const Plot = ({ZarrDS}:{ZarrDS: ZarrDataset}) => {
155156
is4D: state.is4D,
156157
setIsFlat: state.setIsFlat,
157158
})))
158-
159+
const {keyFrameEditor} = useImageExportStore(useShallow(state => ({ keyFrameEditor:state.keyFrameEditor})))
159160
const {plotType, displaceSurface, interpPixels, setPlotType} = usePlotStore(useShallow(state => ({
160161
plotType: state.plotType,
161162
displaceSurface: state.displaceSurface,
@@ -318,6 +319,8 @@ const Plot = ({ZarrDS}:{ZarrDS: ZarrDataset}) => {
318319
<div id='main-canvas-div' className='main-canvas'
319320
style={{width:'100vw'}}
320321
>
322+
<ExportExtent />
323+
{keyFrameEditor && <KeyFrames />}
321324
<TransectNotice />
322325
<AnalysisWG setTexture={setTextures} ZarrDS={ZarrDS}/>
323326
{show && <Colorbar units={stableMetadata?.units} metadata={stableMetadata} valueScales={valueScales}/>}

src/components/ui/ExportExtent.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { useGlobalStore, useImageExportStore } from '@/utils/GlobalStates'
2+
import React from 'react'
3+
import { useShallow } from 'zustand/shallow'
4+
5+
export const ExportExtent = () => {
6+
const {customRes, previewExtent} = useImageExportStore(useShallow(state=> ({
7+
customRes:state.customRes, previewExtent:state.previewExtent
8+
})))
9+
10+
const dpr = useGlobalStore.getState().DPR || window.devicePixelRatio || 1;
11+
12+
const aspectRatio = customRes[0]/customRes[1]
13+
return (
14+
<div style={{
15+
position:'fixed',
16+
width:`calc(100vh*${aspectRatio})`,
17+
height:`100%`,
18+
left:'50%',
19+
top:'50%',
20+
transform:'translate(-50%, -50%)',
21+
display: previewExtent ? '' : "none",
22+
background:"rgba(200,30,30,0.2)",
23+
zIndex:4
24+
}}
25+
>
26+
{/* Horizontal */}
27+
<div
28+
style={{
29+
position:"absolute",
30+
width: '100%',
31+
top:'50%',
32+
borderTop:'1px solid black',
33+
borderBottom:'1px solid black',
34+
}}
35+
/>
36+
<div
37+
style={{
38+
position:"absolute",
39+
width: '100%',
40+
top:'25%',
41+
borderTop:'1px dashed black',
42+
}}
43+
/>
44+
<div
45+
style={{
46+
position:"absolute",
47+
width: '100%',
48+
top:'75%',
49+
borderBottom:'1px dashed black',
50+
}}
51+
/>
52+
{/* Vertical */}
53+
<div
54+
style={{
55+
position:"absolute",
56+
height: '100%',
57+
left:'50%',
58+
borderLeft:'1px solid black',
59+
borderRight:'1px solid black',
60+
}}
61+
/>
62+
<div
63+
style={{
64+
position:"absolute",
65+
height: '100%',
66+
left:'25%',
67+
borderLeft:'1px dashed black',
68+
}}
69+
/>
70+
<div
71+
style={{
72+
position:"absolute",
73+
height: '100%',
74+
left:'75%',
75+
borderRight:'1px dashed black',
76+
}}
77+
/>
78+
</div>
79+
80+
)
81+
}
82+

src/components/ui/ExportImageSettings.tsx

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,27 @@ import {
2121
import { Switch } from './switch';
2222
import Hider from './Hider';
2323
import { Button } from './button';
24-
import KeyFrames from './KeyFrames';
24+
import { BsBoxArrowRight } from "react-icons/bs";
2525

2626
const ExportImageSettings = () => {
2727
const {
2828
includeBackground, includeColorbar, doubleSize, cbarLoc, cbarNum,
2929
useCustomRes, customRes, includeAxis, mainTitle, cbarLabel, cbarUnits, animate, timeRate,
30-
frames, frameRate, orbit, useTime, loopTime, animViz, preview, pingpong
30+
frames, frameRate, orbit, useTime, loopTime, keyFrameEditor, preview, pingpong
3131
} = useImageExportStore(useShallow(state => ({
3232
includeBackground: state.includeBackground, includeColorbar: state.includeColorbar,
3333
doubleSize: state.doubleSize, cbarLoc: state.cbarLoc, cbarNum: state.cbarNum, useCustomRes: state.useCustomRes,
3434
customRes: state.customRes, includeAxis: state.includeAxis, mainTitle: state.mainTitle, cbarLabel: state.cbarLabel,
3535
cbarUnits:state.cbarUnits, animate: state.animate, timeRate:state.timeRate, frames: state.frames, frameRate: state.frameRate,
36-
orbit: state.orbit, useTime:state.useTime, loopTime: state.loopTime, animViz:state.animViz,
36+
orbit: state.orbit, useTime:state.useTime, loopTime: state.loopTime, keyFrameEditor:state.keyFrameEditor,
3737
preview:state.preview, pingpong:state.pingpong
3838
})))
3939

4040
const {ExportImg, EnableExport, setIncludeBackground, setIncludeColorbar,
4141
setDoubleSize, setCbarLoc, setCbarNum, setUseCustomRes, setCustomRes, setIncludeAxis,
4242
setHideAxis, setHideAxisControls, setMainTitle, setCbarLabel, setAnimate,
43-
setFrames, setFrameRate, setTimeRate, setOrbit, setUseTime, setLoopTime, setAnimViz,
44-
setCbarUnits, setPingpong, setPreview} = useImageExportStore.getState()
43+
setFrames, setFrameRate, setTimeRate, setOrbit, setUseTime, setLoopTime, setKeyFrameEditor,
44+
setCbarUnits, setPingpong, setPreview, setPreviewExtent} = useImageExportStore.getState()
4545

4646
interface CapitalizeFn {
4747
(str: string): string;
@@ -58,6 +58,7 @@ const ExportImageSettings = () => {
5858
const [showTitles, setShowTitles] = useState(false)
5959
const [showAnimation, setShowAnimation] = useState(false)
6060
const [showSettings, setShowSettings] = useState(true)
61+
const [previewState, setPreviewState] = useState(false)
6162

6263
useEffect(()=>{
6364
const timeArray = dimArrays[dimArrays.length-3]
@@ -178,6 +179,14 @@ const ExportImageSettings = () => {
178179
<h1>Height</h1>
179180
<Input id='cbarNum' type="number" value={customRes[1]} onChange={e => setCustomRes([customRes[0], parseInt(e.target.value)])}/>
180181
</div>
182+
<Button className={`col-span-2 ${previewState ? 'bg-red-600' : ''}`}
183+
variant='outline'
184+
disableRipple
185+
onPointerEnter={()=>setPreviewExtent(true)}
186+
onPointerLeave={()=>setPreviewExtent(false)}
187+
>
188+
Preview Extent
189+
</Button>
181190
</div>
182191
</Hider>
183192

@@ -215,11 +224,17 @@ const ExportImageSettings = () => {
215224
<Input id="fps" type='number' step={1} value={frameRate} onChange={e => setFrameRate(parseInt(e.target.value))} />
216225
</div>
217226
<div className="grid grid-cols-[auto_60px] items-center gap-2">
218-
<label htmlFor="useOrbit">Ping-Pong</label>
219-
<Switch id="useOrbit" checked={pingpong} onCheckedChange={e=> setPingpong(e)} />
227+
220228

221229
<label htmlFor="useOrbit">Orbit</label>
222230
<Switch id="useOrbit" checked={orbit} onCheckedChange={e=> setOrbit(e)} />
231+
232+
<Hider show={orbit} className='col-span-2'>
233+
<div className="grid grid-cols-[auto_60px] items-center gap-2 mb-2">
234+
<label htmlFor="useOrbit">Ping-Pong</label>
235+
<Switch id="useOrbit" checked={pingpong} onCheckedChange={e=> setPingpong(e)} />
236+
</div>
237+
</Hider>
223238

224239
<label htmlFor="useTime">Animate Time</label>
225240
<Switch id="useTime" checked={useTime} onCheckedChange={e=> setUseTime(e)} />
@@ -235,11 +250,13 @@ const ExportImageSettings = () => {
235250
<div className='border-b my-2' />
236251
</Hider>
237252

238-
<label htmlFor="changeViz">Animate Visuals</label>
239-
<Switch id="changeViz" checked={animViz} onCheckedChange={e=> setAnimViz(e)} />
240-
<Hider show={animViz} className='col-span-2'>
241-
<KeyFrames />
242-
</Hider>
253+
<Button
254+
className='col-span-2 cursor-pointer'
255+
onClick={()=>setKeyFrameEditor(true)}
256+
>
257+
Keyframe Editor <BsBoxArrowRight/>
258+
</Button>
259+
243260
</div>
244261
<div className="grid grid-cols-[auto_60px] items-center gap-2 my-2">
245262
<label htmlFor="usePreview">Export Preview</label>

0 commit comments

Comments
 (0)