Skip to content

Commit a6fb304

Browse files
authored
update UI keyframe editor (#467)
* button groups * tooltips * bump version
1 parent 0c1a455 commit a6fb304

File tree

7 files changed

+230
-61
lines changed

7 files changed

+230
-61
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "browzarr",
3-
"version": "0.1.0",
3+
"version": "0.3.0",
44
"description": "A browser-based visualization toolkit for exploring and analyzing Zarr data stores.",
55
"keywords": [
66
"zarr",

src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default function RootLayout({
2222
<body className="antialiased">
2323
<ClientRoot>
2424
{children}
25-
<Toaster position="top-center" duration={1500}/>
25+
<Toaster richColors expand={true} position="top-center" duration={5000}/>
2626
</ClientRoot>
2727
</body>
2828
</html>

src/components/ui/ExportImageSettings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const ExportImageSettings = () => {
9494
</PopoverTrigger>
9595
<PopoverContent
9696
side="right"
97-
className="w-[200px] select-none"
97+
className="w-[200px] max-h-[90vh] overflow-y-auto [&::-webkit-scrollbar]:hidden select-none"
9898
>
9999
<div className="grid items-center gap-2">
100100
{/* TITLES */}

src/components/ui/HomeButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function HomeButton() {
2020
size="icon"
2121
className="cursor-pointer hover:scale-90 transition-transform duration-100 ease-out"
2222
>
23-
<Image src={logoHome} alt="logoMPI" height={48} />
23+
<Image src={logoHome} alt="logoMPI" height={32} />
2424
</Button>
2525
</Link>
2626
</TooltipTrigger>

src/components/ui/KeyFrames.tsx

Lines changed: 140 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
"use client";
22
import React, {useEffect, useRef, useState } from 'react'
33
import { Button } from './button'
4+
import { ButtonGroup } from "@/components/ui/button-group"
45
import { useImageExportStore, usePlotStore } from '@/utils/GlobalStates'
56
import { useShallow } from 'zustand/shallow'
67
import { Slider } from './slider'
78
import './css/KeyFrames.css'
8-
import { TbDiamondsFilled } from "react-icons/tb";
99
import { Input } from './input';
1010
import { IoCloseCircleSharp } from "react-icons/io5";
11-
import { CiWarning } from "react-icons/ci";
11+
import { FaPlusCircle } from "react-icons/fa";
12+
import { MdDeleteForever } from "react-icons/md";
13+
import { MdPreview } from "react-icons/md";
14+
import { TbKeyframeFilled } from "react-icons/tb";
15+
import { TbKeyframesFilled } from "react-icons/tb";
16+
import { Card, CardContent } from "@/components/ui/card";
17+
import { toast } from "sonner"
18+
import {
19+
Tooltip,
20+
TooltipContent,
21+
TooltipTrigger,
22+
} from "@/components/ui/tooltip";
1223

1324
function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
1425
return keys.reduce((acc, key) => {
@@ -63,6 +74,7 @@ const KeyFrames = () => {
6374
const timeRatio = timeRate/frameRate
6475
const keyFrameList = keyFrames ? Array.from(keyFrames.keys()).sort((a, b) => a - b) : null;
6576
const originalAnimProg = useRef<number | null>(null)
77+
const [MdLg, setMdLg] = useState<"md" | "lg">("md");
6678

6779
useEffect(()=>{ // Clear KeyFrames if it is empty.
6880
if (keyFrameList && keyFrameList.length == 0){
@@ -79,79 +91,151 @@ const KeyFrames = () => {
7991
}
8092
},[])
8193

94+
useEffect(() => {
95+
const handleResize = () => {
96+
setMdLg(window.innerWidth < 768 ? "md" : "lg");
97+
};
98+
handleResize();
99+
window.addEventListener("resize", handleResize);
100+
return () => window.removeEventListener("resize", handleResize);
101+
}, []);
102+
103+
{/* Information */}
104+
useEffect(() => {
105+
if (orbit || useTime) {
106+
toast.warning("Warning!", {
107+
description: "Camera Motion overwritten by orbit!",
108+
action: {
109+
label: "close",
110+
onClick: () => console.log("close"),
111+
},
112+
});
113+
}
114+
}, [orbit, useTime]);
115+
116+
useEffect(() => {
117+
if ((orbit || useTime) && !keyFrames) {
118+
toast.warning("Warning!", {
119+
description: "Keyframe required to preview orbit!",
120+
action: {
121+
label: "close",
122+
onClick: () => console.log("close"),
123+
},
124+
});
125+
}
126+
}, [orbit, useTime, keyFrames]);
127+
82128
return (
83-
<div className='keyframes-container'>
84-
<IoCloseCircleSharp
129+
<Card className='keyframes-container'>
130+
<Tooltip delayDuration={500}>
131+
<TooltipTrigger asChild>
132+
<IoCloseCircleSharp
85133
style={{
86134
position:'absolute',
87135
top:'10px',
88136
left:'10px',
89137
cursor:'pointer'
90138
}}
91139
size={20}
92-
color='var(--play-background)'
93140
onClick={()=>useImageExportStore.getState().setKeyFrameEditor(false)}
94141
/>
95-
<div className='flex justify-between items-center'>
96-
{/* Information */}
97-
<div className={`ml-4 bg-[var(--warning-area)] min-w-[20%] px-4 py-2 rounded-md`}
98-
style={{
99-
visibility:(orbit || useTime) ? "visible" : "hidden"
100-
}}
101-
>
102-
<div style={{display: orbit? "flex" : "none", alignItems:"center", width:"100%", justifyContent:"space-between"}}>
103-
<CiWarning /><b> Camera Motion overwriten by orbit</b><CiWarning />
104-
</div>
105-
<div style={{display: (orbit && !keyFrames)? "flex" : "none", alignItems:"center", width:"100%", justifyContent:"space-between"}}>
106-
<CiWarning /><b>Keyframe required to preview orbit</b><CiWarning />
107-
</div>
108-
109-
</div>
110-
142+
</TooltipTrigger>
143+
<TooltipContent side="top" align="start">
144+
Close Keyframe Editor
145+
</TooltipContent>
146+
</Tooltip>
147+
<CardContent className='flex flex-col gap-1 w-full h-full px-1 py-1'>
148+
<div className='flex flex-wrap justify-center gap-1 ml-6 mr-0 md:ml-8 md:mr-4'>
111149
{/* Buttons */}
112-
<div className='flex justify-center items-center'>
113-
<Button
114-
className='cursor-pointer'
115-
onClick={()=>{SetKeyFrame(currentFrame)}}
116-
>Add Keyframe
117-
</Button>
118-
<Button
119-
disabled={!keyFrameList}
120-
className='cursor-pointer'
121-
onClick={()=>{useImageExportStore.setState({keyFrames: undefined})}}
122-
>Clear Keyframes
123-
</Button>
150+
<ButtonGroup>
151+
<Tooltip delayDuration={500}>
152+
<TooltipTrigger asChild>
153+
<Button
154+
className='cursor-pointer'
155+
size="sm"
156+
variant="outline"
157+
onClick={()=>{SetKeyFrame(currentFrame)}}
158+
>
159+
<FaPlusCircle /> { MdLg === "lg" ? 'Keyframe' : <TbKeyframeFilled/>}
160+
</Button>
161+
</TooltipTrigger>
162+
<TooltipContent side="top" align="start">
163+
Add new Keyframe
164+
</TooltipContent>
165+
</Tooltip>
166+
<Tooltip delayDuration={500}>
167+
<TooltipTrigger asChild>
168+
<Button
169+
disabled={!keyFrameList}
170+
className='cursor-pointer'
171+
size="sm"
172+
variant="outline"
173+
onClick={()=>{useImageExportStore.setState({keyFrames: undefined})}}
174+
>
175+
<MdDeleteForever className='size-6'/> { MdLg === "lg" ? 'Keyframes' : <TbKeyframesFilled/>}
176+
</Button>
177+
</TooltipTrigger>
178+
<TooltipContent side="top" align="start">
179+
Clear all Keyframes
180+
</TooltipContent>
181+
</Tooltip>
182+
183+
<Tooltip delayDuration={500}>
184+
<TooltipTrigger asChild>
124185
<Button
125186
disabled={!keyFrameList}
126187
className='cursor-pointer'
188+
size="sm"
189+
variant="outline"
127190
onClick={()=>{useImageExportStore.getState().PreviewKeyFrames()}}
128-
>Preview Full Animation
191+
>
192+
<MdPreview className='size-6'/> { MdLg === "lg" ? 'Preview' : ''}
129193
</Button>
130-
</div>
131-
194+
</TooltipTrigger>
195+
<TooltipContent side="top" align="start">
196+
Preview full animation
197+
</TooltipContent>
198+
</Tooltip>
199+
</ButtonGroup>
132200
{/* Frame Information */}
133-
<div className='flex justify-center'>
134-
<div className='flex justify-end items-center mr-2'>
135-
<label htmlFor="frames"><b>Frames:</b></label>
136-
<Input className='w-[80px] ml-2' id="frames" type='number' step={1} value={frames} onChange={e => setFrames(Math.max(parseInt(e.target.value),2))} />
137-
</div>
138-
<div className='flex justify-end items-center'>
139-
<b >Frame:</b>
140-
<Input value={currentFrame} type='number'
141-
className='w-[80px] ml-2'
142-
min={1}
143-
step={1}
144-
onChange={e =>parseInt(e.target.value) ? setCurrentFrame(Math.max(parseInt(e.target.value), 1)) : 1}
145-
/>
146-
</div>
147-
</div>
201+
<ButtonGroup >
202+
<Tooltip delayDuration={500}>
203+
<TooltipTrigger asChild>
204+
<Button size="sm" variant="outline">
205+
<TbKeyframesFilled/> { MdLg === "lg" ? 'Frames' : ''}
206+
</Button>
207+
</TooltipTrigger>
208+
<TooltipContent side="top" align="start">
209+
Frames
210+
</TooltipContent>
211+
</Tooltip>
212+
<Input className='w-[80px] h-[32px]' id="frames" type='number' step={1} value={frames} onChange={e => setFrames(Math.max(parseInt(e.target.value),2))} />
213+
</ButtonGroup>
214+
<ButtonGroup >
215+
<Tooltip delayDuration={500}>
216+
<TooltipTrigger asChild>
217+
<Button size="sm" variant="outline">
218+
<TbKeyframeFilled/> { MdLg === "lg" ? 'Frame' : ''}
219+
</Button>
220+
</TooltipTrigger>
221+
<TooltipContent side="top" align="start">
222+
Frame
223+
</TooltipContent>
224+
</Tooltip>
225+
<Input value={currentFrame} type='number'
226+
className='w-[80px] h-[32px]'
227+
min={1}
228+
step={1}
229+
onChange={e =>parseInt(e.target.value) ? setCurrentFrame(Math.max(parseInt(e.target.value), 1)) : 1}
230+
/>
231+
</ButtonGroup>
148232
</div>
149-
<div className="relative w-full my-2 px-2 bg-[var(--background)] drop-shadow-[0_0_4px_var(--notice-shadow)] rounded-lg">
233+
<div className="relative w-full my-2 px-2 drop-shadow-[0_0_4px_var(--notice-shadow)] rounded-lg">
150234
{keyFrameList?.map((frame) => {
151235
const thumbRadius = 8 + 8; //Thumbradius plus padding
152236
const percent = ((frame - 1 )/(frames - 1)) * 100;
153237
return (
154-
<TbDiamondsFilled
238+
<TbKeyframeFilled
155239
key={frame}
156240
style={{
157241
position: "absolute",
@@ -162,7 +246,7 @@ const KeyFrames = () => {
162246
cursor:"pointer",
163247
visibility:percent <= 100 ? "visible" : "hidden"
164248
}}
165-
color='red'
249+
color='orangered'
166250
size={18}
167251
onClick={()=>setCurrentFrame(frame)}
168252
onDoubleClick={()=>{
@@ -187,7 +271,8 @@ const KeyFrames = () => {
187271
/>
188272
</div>
189273

190-
</div>
274+
</CardContent>
275+
</Card>
191276
)
192277
}
193278

src/components/ui/button-group.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Slot } from "@radix-ui/react-slot"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
4+
import { cn } from "@/lib/utils"
5+
import { Separator } from "@/components/ui/separator"
6+
7+
const buttonGroupVariants = cva(
8+
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
9+
{
10+
variants: {
11+
orientation: {
12+
horizontal:
13+
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
14+
vertical:
15+
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
16+
},
17+
},
18+
defaultVariants: {
19+
orientation: "horizontal",
20+
},
21+
}
22+
)
23+
24+
function ButtonGroup({
25+
className,
26+
orientation,
27+
...props
28+
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
29+
return (
30+
<div
31+
role="group"
32+
data-slot="button-group"
33+
data-orientation={orientation}
34+
className={cn(buttonGroupVariants({ orientation }), className)}
35+
{...props}
36+
/>
37+
)
38+
}
39+
40+
function ButtonGroupText({
41+
className,
42+
asChild = false,
43+
...props
44+
}: React.ComponentProps<"div"> & {
45+
asChild?: boolean
46+
}) {
47+
const Comp = asChild ? Slot : "div"
48+
49+
return (
50+
<Comp
51+
className={cn(
52+
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
53+
className
54+
)}
55+
{...props}
56+
/>
57+
)
58+
}
59+
60+
function ButtonGroupSeparator({
61+
className,
62+
orientation = "vertical",
63+
...props
64+
}: React.ComponentProps<typeof Separator>) {
65+
return (
66+
<Separator
67+
data-slot="button-group-separator"
68+
orientation={orientation}
69+
className={cn(
70+
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
71+
className
72+
)}
73+
{...props}
74+
/>
75+
)
76+
}
77+
78+
export {
79+
ButtonGroup,
80+
ButtonGroupSeparator,
81+
ButtonGroupText,
82+
buttonGroupVariants,
83+
}

src/components/ui/css/KeyFrames.css

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
bottom: 10%;
88
z-index: 3;
99
filter: drop-shadow(0px 0px 4px hsla(0, 0%, 50%, 0.2));
10-
padding: 1rem;
10+
padding: 0.25rem;
1111
border: 2px solid hsla(0, 0%, 50%, 0.2);
1212
border-radius: 1rem;
13-
background: var(--background);
13+
background-color: var(--modal-shadow);
14+
backdrop-filter: blur(22px) saturate(180%);
1415
display: grid;
1516
gap:8px;
1617
user-select: none;

0 commit comments

Comments
 (0)