add graph/visualization for interpolated keyframes
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -17,8 +17,13 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm",
|
||||
"placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"shadow-main/10 shadow-[0_0_0_1px]",
|
||||
"flex h-10 w-full items-center justify-between rounded-md",
|
||||
"text-main bg-transparent px-3 py-2 text-sm placeholder:text-main outline-none",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50 transition-all",
|
||||
"focus:outline-none focus:ring-2 focus:ring-offset-1 focus:shadow-primary",
|
||||
"focus:ring-primary",
|
||||
"hover:shadow-primary/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -39,7 +44,8 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-neutral-accent text-popover-foreground shadow-md animate-in fade-in-80",
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-neutral-accent",
|
||||
"text-main shadow-md animate-in fade-in-80",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -58,7 +64,7 @@ const SelectLabel = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm text-main font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -72,7 +78,7 @@ const SelectItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
|
||||
"focus:bg-primary focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"focus:bg-primary focus:text-neutral dark:focus:text-main text-main data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -83,7 +89,7 @@ const SelectItem = React.forwardRef<
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
<SelectPrimitive.ItemText className="text-main">{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
@@ -94,7 +100,7 @@ const SelectSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-slate-800", className)}
|
||||
className={cn("-mx-1 my-1 h-px bg-slate-main", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -8,8 +8,8 @@ import { shallow } from "zustand/shallow";
|
||||
import KeyframeIndicator from "./KeyframeIndicator";
|
||||
import { TIMELINE_SCALE, calculateOffset } from "./common";
|
||||
import { TriangleDownIcon } from "@radix-ui/react-icons";
|
||||
import TrackPropertiesEditor from "./TrackPropertiesEditor";
|
||||
import { flattenedKeyframesByEntity } from "utils";
|
||||
import TrackPropertiesEditor from "./TrackDisplay/TrackPropertiesEditor";
|
||||
import { cn, flattenedKeyframesByEntity } from "utils";
|
||||
|
||||
type TrackProps = {
|
||||
animationData: z.input<typeof AnimationData>;
|
||||
@@ -18,6 +18,10 @@ type TrackProps = {
|
||||
entity: z.input<typeof AnimatedEntity>;
|
||||
};
|
||||
|
||||
const TrackDisplayTypeOptions = ["Default", "Graph"] as const;
|
||||
|
||||
export const TrackDisplayType = z.enum(TrackDisplayTypeOptions);
|
||||
|
||||
const Track: FC<TrackProps> = ({ animationData, index, name, entity }) => {
|
||||
const controls = useDragControls();
|
||||
|
||||
@@ -56,20 +60,18 @@ const Track: FC<TrackProps> = ({ animationData, index, name, entity }) => {
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onPointerDown={(e) => controls.start(e)}
|
||||
className={`h-full transition-all rounded-sm min-w-[200px] p-1 px-2 flex flex-col ${
|
||||
selectedEntity === index ? "bg-highlight" : "bg-neutral-accent"
|
||||
selectedEntity === index
|
||||
? "bg-highlight text-neutral dark:text-main"
|
||||
: "bg-neutral-accent text-main"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<motion.div
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="will-change-transform"
|
||||
className={cn("will-change-transform")}
|
||||
animate={{ rotate: isExpanded ? 0 : -90 }}
|
||||
>
|
||||
<TriangleDownIcon
|
||||
width="32px"
|
||||
height="32px"
|
||||
className="text-main"
|
||||
/>
|
||||
<TriangleDownIcon width="32px" height="32px" />
|
||||
</motion.div>
|
||||
<h3
|
||||
onClick={() =>
|
||||
@@ -77,7 +79,7 @@ const Track: FC<TrackProps> = ({ animationData, index, name, entity }) => {
|
||||
? deselectEntity()
|
||||
: selectEntity(index)
|
||||
}
|
||||
className="text-white-800 h-2 text-base leading-loose font-semibold select-none cursor-pointer"
|
||||
className="h-2 text-base leading-loose font-semibold select-none cursor-pointer"
|
||||
>
|
||||
{name}
|
||||
</h3>
|
||||
|
||||
64
app/src/components/Timeline/TrackDisplay/Graph.tsx
Normal file
64
app/src/components/Timeline/TrackDisplay/Graph.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { FC } from "react";
|
||||
import { extent, bisector } from "d3-array";
|
||||
import { curveNatural } from "@visx/curve";
|
||||
|
||||
import { scaleLinear } from "@visx/scale";
|
||||
import { LinePath } from "@visx/shape";
|
||||
import { Group } from "@visx/group";
|
||||
|
||||
const HEIGHT = 300;
|
||||
const WIDTH = 1200;
|
||||
|
||||
type PropertyValue = {
|
||||
value: number;
|
||||
frame: number;
|
||||
};
|
||||
|
||||
const getValue = (d: PropertyValue) => d.value;
|
||||
const getFrame = (d: PropertyValue) => d.frame;
|
||||
|
||||
const PropertyGraph: FC<{
|
||||
values: Array<{ frame: number; value: number }>;
|
||||
}> = ({ values }) => {
|
||||
const framesScale = scaleLinear({
|
||||
range: [0, WIDTH],
|
||||
domain: extent(values, getFrame) as [number, number],
|
||||
nice: true,
|
||||
});
|
||||
|
||||
const valuesScale = scaleLinear({
|
||||
range: [HEIGHT, 0],
|
||||
domain: extent(values, getValue) as [number, number],
|
||||
nice: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<LinePath
|
||||
curve={curveNatural}
|
||||
stroke="white"
|
||||
strokeWidth={3}
|
||||
data={values}
|
||||
x={(d) => framesScale(getFrame(d)) ?? 0}
|
||||
y={(d) => valuesScale(getValue(d)) ?? 0}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const Graphs: FC<{ values: Array<Array<number>> }> = ({ values }) => {
|
||||
return (
|
||||
<svg width={WIDTH} height={HEIGHT}>
|
||||
{values.map((propertyValues) => (
|
||||
<PropertyGraph
|
||||
values={propertyValues.map((val, index) => ({
|
||||
frame: index,
|
||||
value: val,
|
||||
}))}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Graphs;
|
||||
@@ -11,15 +11,22 @@ import {
|
||||
AnimatedNumberKeyframeIndicator,
|
||||
AnimatedVec2KeyframeIndicator,
|
||||
AnimatedVec3KeyframeIndicator,
|
||||
} from "./KeyframeIndicator";
|
||||
} from "../KeyframeIndicator";
|
||||
import { ToggleGroup, ToggleGroupItem } from "components/ToggleGroup";
|
||||
import { produce } from "immer";
|
||||
import set from "lodash.set";
|
||||
import { useEntitiesStore } from "stores/entities.store";
|
||||
import { shallow } from "zustand/shallow";
|
||||
import { AnimatedValue } from "primitives/Values";
|
||||
import { motion } from "framer-motion";
|
||||
import { ease } from "@unom/style";
|
||||
import { TrackDisplayType } from "../Track";
|
||||
import TrackPropertyGraph from "./TrackPropertyGraph";
|
||||
import { LineChart } from "lucide-react";
|
||||
|
||||
type DisplayState = {
|
||||
type: z.input<typeof TrackDisplayType>;
|
||||
selectedAnimatedProperties: Array<number>;
|
||||
};
|
||||
|
||||
const TrackAnimatedPropertyKeyframes: FC<{
|
||||
animatedProperty: z.input<typeof AnimatedProperty>;
|
||||
@@ -70,12 +77,23 @@ const TrackAnimatedPropertyKeyframes: FC<{
|
||||
const TrackAnimatedProperty: FC<{
|
||||
animatedProperty: z.input<typeof AnimatedProperty>;
|
||||
animationData: z.input<typeof AnimationData>;
|
||||
displayState: DisplayState;
|
||||
index: number;
|
||||
onDisplayStateUpdate: (s: DisplayState) => void;
|
||||
onUpdate: (e: z.input<typeof AnimatedProperty>) => void;
|
||||
}> = ({ animatedProperty, animationData, onUpdate }) => {
|
||||
}> = ({
|
||||
animatedProperty,
|
||||
animationData,
|
||||
onUpdate,
|
||||
displayState,
|
||||
index,
|
||||
onDisplayStateUpdate,
|
||||
}) => {
|
||||
const [selectedDimension, setSelectedDimension] = useState<"x" | "y" | "z">();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
transition={ease.quint(0.8).out}
|
||||
variants={{ enter: { y: 0, opacity: 1 }, from: { y: -10, opacity: 0 } }}
|
||||
className="flex flex-row bg-neutral-accent ml-2 align-center"
|
||||
@@ -115,8 +133,33 @@ const TrackAnimatedProperty: FC<{
|
||||
Z
|
||||
</ToggleGroupItem>
|
||||
)}
|
||||
<ToggleGroupItem
|
||||
selected={displayState.selectedAnimatedProperties.includes(index)}
|
||||
onClick={() => {
|
||||
if (displayState.selectedAnimatedProperties.includes(index)) {
|
||||
onDisplayStateUpdate({
|
||||
...displayState,
|
||||
selectedAnimatedProperties:
|
||||
displayState.selectedAnimatedProperties.filter(
|
||||
(index) => index !== index
|
||||
),
|
||||
});
|
||||
} else {
|
||||
onDisplayStateUpdate({
|
||||
...displayState,
|
||||
selectedAnimatedProperties: [
|
||||
...displayState.selectedAnimatedProperties,
|
||||
index,
|
||||
],
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LineChart />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<TrackAnimatedPropertyKeyframes
|
||||
selectedDimension={
|
||||
@@ -153,30 +196,45 @@ const TrackPropertiesEditor: FC<{
|
||||
|
||||
const parsedEntity = AnimatedEntity.parse(nextValue);
|
||||
|
||||
console.log("reacreated callback");
|
||||
|
||||
entitiesStore.updateEntityById(parsedEntity.id, parsedEntity);
|
||||
},
|
||||
[entity]
|
||||
);
|
||||
|
||||
const [displayState, setDisplayState] = useState<DisplayState>({
|
||||
type: TrackDisplayType.Enum.Default,
|
||||
selectedAnimatedProperties: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate="enter"
|
||||
initial="from"
|
||||
variants={{ enter: {}, from: {} }}
|
||||
transition={{ staggerChildren: 0.05 }}
|
||||
layout
|
||||
className="flex flex-col gap-1"
|
||||
>
|
||||
{animatedProperties.map((animatedProperty, index) => (
|
||||
<TrackAnimatedProperty
|
||||
onUpdate={handleUpdate}
|
||||
<motion.div layout className="flex flex-row">
|
||||
<motion.div
|
||||
animate="enter"
|
||||
initial="from"
|
||||
variants={{ enter: {}, from: {} }}
|
||||
transition={{ staggerChildren: 0.05 }}
|
||||
className="flex flex-col gap-1"
|
||||
>
|
||||
{animatedProperties.map((animatedProperty, index) => (
|
||||
<TrackAnimatedProperty
|
||||
index={index}
|
||||
onDisplayStateUpdate={setDisplayState}
|
||||
displayState={displayState}
|
||||
onUpdate={handleUpdate}
|
||||
animationData={entity.animation_data}
|
||||
key={index}
|
||||
animatedProperty={animatedProperty}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
{displayState.selectedAnimatedProperties.length > 0 && (
|
||||
<TrackPropertyGraph
|
||||
animationData={entity.animation_data}
|
||||
key={index}
|
||||
animatedProperty={animatedProperty}
|
||||
animatedProperties={displayState.selectedAnimatedProperties.map(
|
||||
(index) => animatedProperties[index]
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
108
app/src/components/Timeline/TrackDisplay/TrackPropertyGraph.tsx
Normal file
108
app/src/components/Timeline/TrackDisplay/TrackPropertyGraph.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { AnimationData } from "primitives/AnimatedEntities";
|
||||
import { AnimatedProperty } from "primitives/AnimatedProperty";
|
||||
import { AnimatedValue, ValueType } from "primitives/Values";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import Graph from "./Graph";
|
||||
|
||||
type TrackPropertyPathProps = {
|
||||
animatedProperties: Array<z.input<typeof AnimatedProperty>>;
|
||||
animationData: z.input<typeof AnimationData>;
|
||||
};
|
||||
|
||||
const TrackPropertyGraph: FC<TrackPropertyPathProps> = ({
|
||||
animatedProperties,
|
||||
animationData,
|
||||
}) => {
|
||||
const [values, setValues] = useState<Array<Array<number>>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const tasks: Array<Promise<Array<Array<number>>>> = [];
|
||||
|
||||
animatedProperties.forEach((animatedProperty) => {
|
||||
animatedProperty.animatedValue.type;
|
||||
const animatedValue = animatedProperty.animatedValue;
|
||||
|
||||
const commonValues: {
|
||||
animatedValue: z.input<typeof AnimatedValue>;
|
||||
startFrame: number;
|
||||
endFrame: number;
|
||||
fps: number;
|
||||
animationData: z.input<typeof AnimationData>;
|
||||
} = {
|
||||
animatedValue: AnimatedValue.parse(animatedValue),
|
||||
startFrame: 0,
|
||||
endFrame: 600,
|
||||
fps: 60,
|
||||
animationData: AnimationData.parse(animationData),
|
||||
};
|
||||
|
||||
switch (animatedValue.type) {
|
||||
case ValueType.Enum.Number:
|
||||
tasks.push(
|
||||
invoke(
|
||||
"get_values_at_frame_range_from_animated_float",
|
||||
commonValues
|
||||
).then((data) => {
|
||||
const numbers = data as Array<number>;
|
||||
|
||||
return [numbers];
|
||||
})
|
||||
);
|
||||
break;
|
||||
case ValueType.Enum.Vec2:
|
||||
tasks.push(
|
||||
invoke(
|
||||
"get_values_at_frame_range_from_animated_float_vec2",
|
||||
commonValues
|
||||
).then((data) => {
|
||||
const vectors = data as [Array<number>, Array<number>];
|
||||
|
||||
const xValues = vectors.map((vec2) => vec2[0]);
|
||||
const yValues = vectors.map((vec2) => vec2[1]);
|
||||
|
||||
return [xValues, yValues];
|
||||
})
|
||||
);
|
||||
break;
|
||||
|
||||
case ValueType.Enum.Vec3:
|
||||
tasks.push(
|
||||
invoke(
|
||||
"get_values_at_frame_range_from_animated_float_vec3",
|
||||
commonValues
|
||||
).then((data) => {
|
||||
const vectors = data as [
|
||||
Array<number>,
|
||||
Array<number>,
|
||||
Array<number>
|
||||
];
|
||||
|
||||
const xValues = vectors.map((vec2) => vec2[0]);
|
||||
const yValues = vectors.map((vec2) => vec2[1]);
|
||||
const zValues = vectors.map((vec2) => vec2[2]);
|
||||
|
||||
return [xValues, yValues, zValues];
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
Promise.all(tasks).then((values) => {
|
||||
const flatValues = values.flat();
|
||||
|
||||
console.log("flattened Values", flatValues);
|
||||
setValues(flatValues);
|
||||
});
|
||||
}, animatedProperties);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Graph values={values} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrackPropertyGraph;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { Interpolation } from "./Interpolation";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
export const Keyframe = z.object({
|
||||
id: z.string().uuid(),
|
||||
|
||||
@@ -5,6 +5,8 @@ import { v4 as uuid } from "uuid";
|
||||
export const Vec2 = z.array(z.number()).length(2);
|
||||
export const Vec3 = z.array(z.number()).length(3);
|
||||
|
||||
|
||||
|
||||
const ValueTypeOptions = ["Vec2", "Vec3", "Number"] as const;
|
||||
|
||||
export const ValueType = z.enum(ValueTypeOptions);
|
||||
|
||||
@@ -13,24 +13,16 @@
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
span {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-blue-600 underline;
|
||||
}
|
||||
|
||||
select,
|
||||
input {
|
||||
@apply box-border bg-transparent shadow-main/10 hover:shadow-primary/50
|
||||
focus:ring-primary focus:ring-2 focus:ring-offset-2
|
||||
focus:ring-primary focus:ring-2 focus:ring-offset-1
|
||||
focus:shadow-primary selection:bg-secondary selection:text-black
|
||||
outline-none px-3 py-2 rounded-md shadow-[0_0_0_1px];
|
||||
}
|
||||
|
||||
input {
|
||||
@apply appearance-none items-center justify-center
|
||||
outline-none px-3 py-2 rounded-md shadow-[0_0_0_1px]
|
||||
appearance-none items-center justify-center
|
||||
w-full text-base leading-none transition-all;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user