add graph/visualization for interpolated keyframes
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2023-06-26 23:41:07 +02:00
parent cae187b939
commit f237d73016
13 changed files with 768 additions and 53 deletions

View File

@@ -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}
/>
));

View File

@@ -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>

View 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;

View File

@@ -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>
);
};

View 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;

View File

@@ -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(),

View File

@@ -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);

View File

@@ -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;
}