ui improvements
add timeline animations
This commit is contained in:
parent
8d1f949280
commit
e3098c4400
@ -19,10 +19,12 @@
|
|||||||
"@radix-ui/react-toolbar": "^1.0.3",
|
"@radix-ui/react-toolbar": "^1.0.3",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@tauri-apps/api": "^1.3.0",
|
||||||
"@tempblade/common": "^2.0.1",
|
"@tempblade/common": "^2.0.1",
|
||||||
|
"@types/lodash.set": "^4.3.7",
|
||||||
"@unom/style": "^0.2.14",
|
"@unom/style": "^0.2.14",
|
||||||
"canvaskit-wasm": "^0.38.1",
|
"canvaskit-wasm": "^0.38.1",
|
||||||
"framer-motion": "^10.12.12",
|
"framer-motion": "^10.12.12",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
|
"lodash.set": "^4.3.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
@ -27,8 +27,8 @@ export default function App() {
|
|||||||
<MenuBar />
|
<MenuBar />
|
||||||
<div className="flex flex-row w-full h-full">
|
<div className="flex flex-row w-full h-full">
|
||||||
<ToolBar />
|
<ToolBar />
|
||||||
<div className="flex flex-col ml-4 mr-4 mt-4 w-full h-full overflow-x-hidden">
|
<div className="flex flex-col ml-4 mr-4 mt-4 h-full w-full overflow-y-auto">
|
||||||
<div className="flex gap-4 flex-col lg:flex-row mb-4 justify-center items-center">
|
<div className="flex w-full gap-4 flex-col lg:flex-row mb-4 justify-center items-center">
|
||||||
<Canvas />
|
<Canvas />
|
||||||
<PropertiesContainer>
|
<PropertiesContainer>
|
||||||
<Properties />
|
<Properties />
|
||||||
|
@ -26,18 +26,17 @@ const CanvasComponent: FC<CanvasProps> = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
<div
|
className="flex items-center justify-center"
|
||||||
className="flex items-center justify-center"
|
style={{ width: "100%", height: "100%" }}
|
||||||
style={{ width: "100%", height: "500px" }}
|
>
|
||||||
>
|
<canvas
|
||||||
<canvas
|
style={{ width: "100%", height: "100%" }}
|
||||||
className="aspect-video h-full"
|
className="h-full object-contain"
|
||||||
height={720}
|
height={720}
|
||||||
width={1280}
|
width={1280}
|
||||||
ref={canvas}
|
ref={canvas}
|
||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,51 +1,111 @@
|
|||||||
import { ease } from "@unom/style";
|
import { ease } from "@unom/style";
|
||||||
import { motion } from "framer-motion";
|
import { PanInfo, motion } from "framer-motion";
|
||||||
import { AnimationData } from "primitives/AnimatedEntities";
|
import { AnimationData } from "primitives/AnimatedEntities";
|
||||||
import { Keyframe } from "primitives/Keyframe";
|
import { Keyframe } from "primitives/Keyframe";
|
||||||
import { FC } from "react";
|
import { FC, useCallback, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TIMELINE_SCALE } from "./common";
|
import { TIMELINE_SCALE, calculateOffset } from "./common";
|
||||||
import { AnimatedNumber, AnimatedVec2, AnimatedVec3 } from "primitives/Values";
|
import { AnimatedNumber, AnimatedVec2, AnimatedVec3 } from "primitives/Values";
|
||||||
import { useKeyframeStore } from "stores/keyframe.store";
|
import { useKeyframeStore } from "stores/keyframe.store";
|
||||||
|
import { produce } from "immer";
|
||||||
|
|
||||||
const KeyframeIndicator: FC<{
|
const KeyframeIndicator: FC<{
|
||||||
keyframe: z.input<typeof Keyframe>;
|
keyframe: z.input<typeof Keyframe>;
|
||||||
animationData: z.input<typeof AnimationData>;
|
animationData: z.input<typeof AnimationData>;
|
||||||
}> = ({ keyframe, animationData }) => {
|
onUpdate?: (e: z.input<typeof Keyframe>) => void;
|
||||||
|
}> = ({ keyframe, animationData, onUpdate }) => {
|
||||||
const { selectedKeyframe, selectKeyframe, deselectKeyframe } =
|
const { selectedKeyframe, selectKeyframe, deselectKeyframe } =
|
||||||
useKeyframeStore();
|
useKeyframeStore();
|
||||||
|
|
||||||
const selected = selectedKeyframe === keyframe.id;
|
const selected = selectedKeyframe === keyframe.id;
|
||||||
|
|
||||||
|
const handleUpdate = useCallback(
|
||||||
|
(info: PanInfo) => {
|
||||||
|
if (onUpdate) {
|
||||||
|
let offset = info.offset.x;
|
||||||
|
|
||||||
|
offset = calculateOffset(offset);
|
||||||
|
|
||||||
|
offset += keyframe.offset;
|
||||||
|
|
||||||
|
onUpdate({ ...keyframe, offset: offset < 0 ? 0 : offset });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onUpdate, animationData, keyframe]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isDragged, setIsDragged] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
drag="x"
|
drag="x"
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
variants={{
|
||||||
|
enter: {},
|
||||||
|
from: {},
|
||||||
|
exit: {},
|
||||||
|
tap: {},
|
||||||
|
drag: {},
|
||||||
|
}}
|
||||||
data-selected={selected}
|
data-selected={selected}
|
||||||
|
onDragStart={() => setIsDragged(true)}
|
||||||
|
onDragEnd={(e, info) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragged(false);
|
||||||
|
if (onUpdate) {
|
||||||
|
handleUpdate(info);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
dragConstraints={{ left: 0 }}
|
dragConstraints={{ left: 0 }}
|
||||||
|
initial={{
|
||||||
|
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
|
||||||
|
scale: 0,
|
||||||
|
}}
|
||||||
|
whileTap={{ scale: 1.1 }}
|
||||||
animate={{
|
animate={{
|
||||||
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
|
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
|
||||||
|
scale: 1,
|
||||||
}}
|
}}
|
||||||
transition={ease.quint(0.4).out}
|
transition={ease.quint(0.4).out}
|
||||||
style={{
|
onClick={() => {
|
||||||
clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
|
if (!isDragged) {
|
||||||
|
selected ? deselectKeyframe() : selectKeyframe(keyframe.id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onClick={() =>
|
className="h-full absolute z-30 select-none w-3 flex items-center justify-center filter
|
||||||
selected ? deselectKeyframe() : selectKeyframe(keyframe.id)
|
data-[selected=true]:drop-shadow-[0px_2px_6px_rgba(255,255,255,1)] transition-colors"
|
||||||
}
|
>
|
||||||
className="bg-indigo-500 data-[selected=true]:bg-indigo-300 absolute w-2 h-2 z-30 select-none"
|
<span
|
||||||
/>
|
data-selected={selected}
|
||||||
|
className="bg-gray-200
|
||||||
|
data-[selected=true]:bg-indigo-600
|
||||||
|
h-full transition-colors"
|
||||||
|
style={{
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnimatedNumberKeyframeIndicator: FC<{
|
const AnimatedNumberKeyframeIndicator: FC<{
|
||||||
animatedNumber: z.input<typeof AnimatedNumber>;
|
animatedNumber: z.input<typeof AnimatedNumber>;
|
||||||
animationData: z.input<typeof AnimationData>;
|
animationData: z.input<typeof AnimationData>;
|
||||||
}> = ({ animatedNumber, animationData }) => {
|
onUpdate: (e: z.input<typeof AnimatedNumber>) => void;
|
||||||
|
}> = ({ animatedNumber, animationData, onUpdate }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{animatedNumber.keyframes.values.map((keyframe) => (
|
{animatedNumber.keyframes.values.map((keyframe, index) => (
|
||||||
<KeyframeIndicator
|
<KeyframeIndicator
|
||||||
|
onUpdate={(keyframe) =>
|
||||||
|
onUpdate(
|
||||||
|
produce(animatedNumber, (draft) => {
|
||||||
|
draft.keyframes.values[index] = keyframe;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
key={keyframe.id}
|
key={keyframe.id}
|
||||||
keyframe={keyframe}
|
keyframe={keyframe}
|
||||||
animationData={animationData}
|
animationData={animationData}
|
||||||
@ -65,11 +125,29 @@ const AnimatedVec2KeyframeIndicator: FC<{
|
|||||||
animatedVec2: z.input<typeof AnimatedVec2>;
|
animatedVec2: z.input<typeof AnimatedVec2>;
|
||||||
dimension?: DimensionsVec2;
|
dimension?: DimensionsVec2;
|
||||||
animationData: z.input<typeof AnimationData>;
|
animationData: z.input<typeof AnimationData>;
|
||||||
}> = ({ animatedVec2, animationData, dimension }) => {
|
onUpdate: (e: z.input<typeof AnimatedVec2>) => void;
|
||||||
|
}> = ({ animatedVec2, animationData, dimension, onUpdate }) => {
|
||||||
|
const handleUpdate = useCallback(
|
||||||
|
(
|
||||||
|
animatedNumber: z.input<typeof AnimatedNumber>,
|
||||||
|
dimensionIndex: number
|
||||||
|
) => {
|
||||||
|
onUpdate(
|
||||||
|
produce(animatedVec2, (draft) => {
|
||||||
|
draft.keyframes[dimensionIndex] = animatedNumber;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[animatedVec2]
|
||||||
|
);
|
||||||
|
|
||||||
if (dimension) {
|
if (dimension) {
|
||||||
return (
|
return (
|
||||||
<AnimatedNumberKeyframeIndicator
|
<AnimatedNumberKeyframeIndicator
|
||||||
animationData={animationData}
|
animationData={animationData}
|
||||||
|
onUpdate={(animatedNumber) =>
|
||||||
|
handleUpdate(animatedNumber, VEC2_DIMENSION_INDEX_MAPPING[dimension])
|
||||||
|
}
|
||||||
animatedNumber={
|
animatedNumber={
|
||||||
animatedVec2.keyframes[VEC2_DIMENSION_INDEX_MAPPING[dimension]]
|
animatedVec2.keyframes[VEC2_DIMENSION_INDEX_MAPPING[dimension]]
|
||||||
}
|
}
|
||||||
@ -81,6 +159,7 @@ const AnimatedVec2KeyframeIndicator: FC<{
|
|||||||
<>
|
<>
|
||||||
{animatedVec2.keyframes.map((animatedNumber, index) => (
|
{animatedVec2.keyframes.map((animatedNumber, index) => (
|
||||||
<AnimatedNumberKeyframeIndicator
|
<AnimatedNumberKeyframeIndicator
|
||||||
|
onUpdate={(animatedNumber) => handleUpdate(animatedNumber, index)}
|
||||||
key={index}
|
key={index}
|
||||||
animatedNumber={animatedNumber}
|
animatedNumber={animatedNumber}
|
||||||
animationData={animationData}
|
animationData={animationData}
|
||||||
@ -101,11 +180,29 @@ const AnimatedVec3KeyframeIndicator: FC<{
|
|||||||
animatedVec3: z.input<typeof AnimatedVec3>;
|
animatedVec3: z.input<typeof AnimatedVec3>;
|
||||||
animationData: z.input<typeof AnimationData>;
|
animationData: z.input<typeof AnimationData>;
|
||||||
dimension?: DimensionsVec3;
|
dimension?: DimensionsVec3;
|
||||||
}> = ({ animatedVec3, animationData, dimension }) => {
|
onUpdate: (e: z.input<typeof AnimatedVec3>) => void;
|
||||||
|
}> = ({ animatedVec3, animationData, dimension, onUpdate }) => {
|
||||||
|
const handleUpdate = useCallback(
|
||||||
|
(
|
||||||
|
animatedNumber: z.input<typeof AnimatedNumber>,
|
||||||
|
dimensionIndex: number
|
||||||
|
) => {
|
||||||
|
onUpdate(
|
||||||
|
produce(animatedVec3, (draft) => {
|
||||||
|
draft.keyframes[dimensionIndex] = animatedNumber;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[animatedVec3]
|
||||||
|
);
|
||||||
|
|
||||||
if (dimension) {
|
if (dimension) {
|
||||||
return (
|
return (
|
||||||
<AnimatedNumberKeyframeIndicator
|
<AnimatedNumberKeyframeIndicator
|
||||||
animationData={animationData}
|
animationData={animationData}
|
||||||
|
onUpdate={(animatedNumber) =>
|
||||||
|
handleUpdate(animatedNumber, VEC3_DIMENSION_INDEX_MAPPING[dimension])
|
||||||
|
}
|
||||||
animatedNumber={
|
animatedNumber={
|
||||||
animatedVec3.keyframes[VEC3_DIMENSION_INDEX_MAPPING[dimension]]
|
animatedVec3.keyframes[VEC3_DIMENSION_INDEX_MAPPING[dimension]]
|
||||||
}
|
}
|
||||||
@ -118,6 +215,7 @@ const AnimatedVec3KeyframeIndicator: FC<{
|
|||||||
{animatedVec3.keyframes.map((animatedNumber, index) => (
|
{animatedVec3.keyframes.map((animatedNumber, index) => (
|
||||||
<AnimatedNumberKeyframeIndicator
|
<AnimatedNumberKeyframeIndicator
|
||||||
key={index}
|
key={index}
|
||||||
|
onUpdate={(animatedNumber) => handleUpdate(animatedNumber, index)}
|
||||||
animatedNumber={animatedNumber}
|
animatedNumber={animatedNumber}
|
||||||
animationData={animationData}
|
animationData={animationData}
|
||||||
/>
|
/>
|
||||||
|
@ -6,10 +6,7 @@ import { FC, useState } from "react";
|
|||||||
import { useEntitiesStore } from "stores/entities.store";
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
import KeyframeIndicator, {
|
import KeyframeIndicator from "./KeyframeIndicator";
|
||||||
AnimatedVec2KeyframeIndicator,
|
|
||||||
AnimatedVec3KeyframeIndicator,
|
|
||||||
} from "./KeyframeIndicator";
|
|
||||||
import { TIMELINE_SCALE, calculateOffset } from "./common";
|
import { TIMELINE_SCALE, calculateOffset } from "./common";
|
||||||
import { TriangleDownIcon } from "@radix-ui/react-icons";
|
import { TriangleDownIcon } from "@radix-ui/react-icons";
|
||||||
import TrackPropertiesEditor from "./TrackPropertiesEditor";
|
import TrackPropertiesEditor from "./TrackPropertiesEditor";
|
||||||
@ -49,9 +46,14 @@ const Track: FC<TrackProps> = ({
|
|||||||
value={entity}
|
value={entity}
|
||||||
dragListener={false}
|
dragListener={false}
|
||||||
dragControls={controls}
|
dragControls={controls}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
className="min-h-8 relative flex flex-1 flex-col gap-1 select-none"
|
className="min-h-8 relative flex flex-1 flex-col gap-1 select-none"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row gap-1 select-none">
|
<motion.div
|
||||||
|
layout
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
className="flex flex-row gap-1 select-none"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onPointerDown={(e) => controls.start(e)}
|
onPointerDown={(e) => controls.start(e)}
|
||||||
@ -132,10 +134,10 @@ const Track: FC<TrackProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="z-10 w-4 bg-slate-500 h-8 absolute rounded-md select-none cursor-w-resize"
|
className="z-10 w-4 bg-slate-500 h-8 top-1 absolute rounded-md select-none cursor-w-resize"
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="z-10 w-4 bg-slate-500 h-8 absolute rounded-md select-none cursor-e-resize"
|
className="z-10 w-4 bg-slate-500 h-8 top-1 absolute rounded-md select-none cursor-e-resize"
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
drag="x"
|
drag="x"
|
||||||
animate={{
|
animate={{
|
||||||
@ -194,10 +196,10 @@ const Track: FC<TrackProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="z-5 h-8 absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none cursor-grab"
|
className="z-5 h-8 top-1 absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none cursor-grab"
|
||||||
></motion.div>
|
></motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
{isExpanded && <TrackPropertiesEditor entity={entity} />}
|
{isExpanded && <TrackPropertiesEditor entity={entity} />}
|
||||||
</Reorder.Item>
|
</Reorder.Item>
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
} from "primitives/AnimatedEntities";
|
} from "primitives/AnimatedEntities";
|
||||||
import { AnimatedProperty } from "primitives/AnimatedProperty";
|
import { AnimatedProperty } from "primitives/AnimatedProperty";
|
||||||
import { AnimatedVec2, ValueType } from "primitives/Values";
|
import { AnimatedVec2, ValueType } from "primitives/Values";
|
||||||
import { FC, useMemo, useState } from "react";
|
import { FC, useCallback, useMemo, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
AnimatedNumberKeyframeIndicator,
|
AnimatedNumberKeyframeIndicator,
|
||||||
@ -13,18 +13,34 @@ import {
|
|||||||
AnimatedVec3KeyframeIndicator,
|
AnimatedVec3KeyframeIndicator,
|
||||||
} from "./KeyframeIndicator";
|
} from "./KeyframeIndicator";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "components/ToggleGroup";
|
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";
|
||||||
|
|
||||||
const TrackAnimatedPropertyKeyframes: FC<{
|
const TrackAnimatedPropertyKeyframes: FC<{
|
||||||
animatedProperty: z.input<typeof AnimatedProperty>;
|
animatedProperty: z.input<typeof AnimatedProperty>;
|
||||||
animationData: z.input<typeof AnimationData>;
|
animationData: z.input<typeof AnimationData>;
|
||||||
|
onUpdate: (animatedProperty: z.input<typeof AnimatedProperty>) => void;
|
||||||
selectedDimension?: "x" | "y" | "z";
|
selectedDimension?: "x" | "y" | "z";
|
||||||
}> = ({ animatedProperty, animationData, selectedDimension }) => {
|
}> = ({ animatedProperty, animationData, selectedDimension, onUpdate }) => {
|
||||||
|
const handleUpdate = useCallback(
|
||||||
|
(animatedValue: z.input<typeof AnimatedValue>) => {
|
||||||
|
onUpdate({ ...animatedProperty, animatedValue });
|
||||||
|
},
|
||||||
|
[onUpdate, animatedProperty]
|
||||||
|
);
|
||||||
|
|
||||||
switch (animatedProperty.animatedValue.type) {
|
switch (animatedProperty.animatedValue.type) {
|
||||||
case "Number":
|
case "Number":
|
||||||
return (
|
return (
|
||||||
<AnimatedNumberKeyframeIndicator
|
<AnimatedNumberKeyframeIndicator
|
||||||
animatedNumber={animatedProperty.animatedValue}
|
animatedNumber={animatedProperty.animatedValue}
|
||||||
animationData={animationData}
|
animationData={animationData}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "Vec2":
|
case "Vec2":
|
||||||
@ -33,6 +49,7 @@ const TrackAnimatedPropertyKeyframes: FC<{
|
|||||||
dimension={selectedDimension !== "z" ? selectedDimension : undefined}
|
dimension={selectedDimension !== "z" ? selectedDimension : undefined}
|
||||||
animatedVec2={animatedProperty.animatedValue}
|
animatedVec2={animatedProperty.animatedValue}
|
||||||
animationData={animationData}
|
animationData={animationData}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -42,6 +59,7 @@ const TrackAnimatedPropertyKeyframes: FC<{
|
|||||||
dimension={selectedDimension}
|
dimension={selectedDimension}
|
||||||
animatedVec3={animatedProperty.animatedValue}
|
animatedVec3={animatedProperty.animatedValue}
|
||||||
animationData={animationData}
|
animationData={animationData}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
@ -52,14 +70,18 @@ const TrackAnimatedPropertyKeyframes: FC<{
|
|||||||
const TrackAnimatedProperty: FC<{
|
const TrackAnimatedProperty: FC<{
|
||||||
animatedProperty: z.input<typeof AnimatedProperty>;
|
animatedProperty: z.input<typeof AnimatedProperty>;
|
||||||
animationData: z.input<typeof AnimationData>;
|
animationData: z.input<typeof AnimationData>;
|
||||||
trackIndex: number;
|
onUpdate: (e: z.input<typeof AnimatedProperty>) => void;
|
||||||
}> = ({ animatedProperty, animationData }) => {
|
}> = ({ animatedProperty, animationData, onUpdate }) => {
|
||||||
const [selectedDimension, setSelectedDimension] = useState<"x" | "y" | "z">();
|
const [selectedDimension, setSelectedDimension] = useState<"x" | "y" | "z">();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row">
|
<motion.div
|
||||||
<div className="min-w-[200px] flex flex-row justify-between">
|
transition={ease.quint(0.8).out}
|
||||||
<h3>{animatedProperty.label}</h3>
|
variants={{ enter: { y: 0, opacity: 1 }, from: { y: -10, opacity: 0 } }}
|
||||||
|
className="flex flex-row bg-slate-900 ml-2 align-center"
|
||||||
|
>
|
||||||
|
<div className="min-w-[195px] flex flex-row justify-between px-2">
|
||||||
|
<h4>{animatedProperty.label}</h4>
|
||||||
<ToggleGroup>
|
<ToggleGroup>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
onClick={() => setSelectedDimension("x")}
|
onClick={() => setSelectedDimension("x")}
|
||||||
@ -90,36 +112,59 @@ const TrackAnimatedProperty: FC<{
|
|||||||
? selectedDimension
|
? selectedDimension
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onUpdate={onUpdate}
|
||||||
animatedProperty={animatedProperty}
|
animatedProperty={animatedProperty}
|
||||||
animationData={animationData}
|
animationData={animationData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TrackPropertiesEditor: FC<{ entity: z.input<typeof AnimatedEntity> }> = ({
|
const TrackPropertiesEditor: FC<{
|
||||||
entity,
|
entity: z.input<typeof AnimatedEntity>;
|
||||||
}) => {
|
}> = ({ entity }) => {
|
||||||
const animatedProperties = useMemo(
|
const animatedProperties = useMemo(
|
||||||
() => getAnimatedPropertiesByAnimatedEntity(entity),
|
() => getAnimatedPropertiesByAnimatedEntity(entity),
|
||||||
[entity]
|
[entity]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleUpdate = useCallback(
|
||||||
|
(animatedProperty: z.input<typeof AnimatedProperty>) => {
|
||||||
|
const entitiesStore = useEntitiesStore.getState();
|
||||||
|
|
||||||
|
const nextValue = produce(entity, (draft) => {
|
||||||
|
const animatedValue = animatedProperty.animatedValue;
|
||||||
|
|
||||||
|
set(draft, animatedProperty.propertyPath, animatedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedEntity = AnimatedEntity.parse(nextValue);
|
||||||
|
|
||||||
|
entitiesStore.updateEntityById(parsedEntity.id, parsedEntity);
|
||||||
|
},
|
||||||
|
[entity]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<motion.div
|
||||||
|
animate="enter"
|
||||||
|
initial="from"
|
||||||
|
variants={{ enter: {}, from: {} }}
|
||||||
|
transition={{ staggerChildren: 0.05 }}
|
||||||
|
layout
|
||||||
|
className="flex flex-col gap-1"
|
||||||
|
>
|
||||||
{animatedProperties.map((animatedProperty, index) => (
|
{animatedProperties.map((animatedProperty, index) => (
|
||||||
<TrackAnimatedProperty
|
<TrackAnimatedProperty
|
||||||
trackIndex={index}
|
onUpdate={handleUpdate}
|
||||||
animationData={entity.animation_data}
|
animationData={entity.animation_data}
|
||||||
key={index}
|
key={index}
|
||||||
animatedProperty={animatedProperty}
|
animatedProperty={animatedProperty}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TrackPropertiesEditor;
|
export default TrackPropertiesEditor;
|
||||||
|
|
||||||
AnimatedVec2._def.typeName;
|
|
||||||
|
@ -26,7 +26,7 @@ const Timeline: FC<TimelineProps> = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col p-4 w-full border transition-colors focus-within:border-gray-400 border-gray-600 rounded-md">
|
<div className="flex flex-col p-4 border transition-colors focus-within:border-gray-400 border-gray-600 rounded-md">
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<button onClick={() => setPlaying(true)} className="w-8 h-8">
|
<button onClick={() => setPlaying(true)} className="w-8 h-8">
|
||||||
@ -38,13 +38,13 @@ const Timeline: FC<TimelineProps> = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Timestamp />
|
<Timestamp />
|
||||||
</div>
|
</div>
|
||||||
<div className="gap-1 flex flex-col overflow-y-hidden">
|
<div className="gap-1 w-full flex flex-col overflow-x-auto">
|
||||||
<div className="z-20 flex flex-row gap-2">
|
<div className="z-20 flex flex-row gap-2">
|
||||||
<div className="flex-shrink-0 min-w-[200px]" />
|
<div className="flex-shrink-0 min-w-[200px]" />
|
||||||
<TimePicker />
|
<TimePicker />
|
||||||
</div>
|
</div>
|
||||||
<Reorder.Group
|
<Reorder.Group
|
||||||
className="gap-1 flex-1 flex flex-col"
|
className="gap-1 flex flex-col"
|
||||||
values={entities}
|
values={entities}
|
||||||
onReorder={setEntities}
|
onReorder={setEntities}
|
||||||
>
|
>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
import * as ToggleGroupComponents from "@radix-ui/react-toggle-group";
|
import * as ToggleGroupComponents from "@radix-ui/react-toggle-group";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
const ToggleGroupItem: FC<{
|
const ToggleGroupItem: FC<{
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -9,23 +10,26 @@ const ToggleGroupItem: FC<{
|
|||||||
return (
|
return (
|
||||||
<ToggleGroupComponents.Item
|
<ToggleGroupComponents.Item
|
||||||
data-selected={selected}
|
data-selected={selected}
|
||||||
|
asChild
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="hover:bg-indigo-400 text-white data-[selected=true]:bg-indigo-600
|
className="hover:bg-indigo-600 text-white data-[selected=true]:bg-indigo-700
|
||||||
data-[selected=true]:text-indigo-200 flex h-6 w-6
|
data-[selected=true]:text-indigo-200 flex h-6 w-6
|
||||||
items-center justify-center bg-slate-900 text-sm leading-4
|
items-center justify-center bg-slate-900 text-sm leading-4
|
||||||
first:rounded-l last:rounded-r focus:z-10 focus:shadow-[0_0_0_2px] focus:shadow-black
|
first:rounded-l last:rounded-r focus:z-10 focus:shadow-[0_0_0_2px] focus:shadow-black
|
||||||
focus:outline-none"
|
focus:outline-none transition-colors"
|
||||||
value="left"
|
value="left"
|
||||||
aria-label="Left aligned"
|
aria-label="Left aligned"
|
||||||
>
|
>
|
||||||
{children}
|
<motion.button animate={{ scale: 1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
{children}
|
||||||
|
</motion.button>
|
||||||
</ToggleGroupComponents.Item>
|
</ToggleGroupComponents.Item>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ToggleGroup: FC<{ children: ReactNode }> = ({ children }) => (
|
const ToggleGroup: FC<{ children: ReactNode }> = ({ children }) => (
|
||||||
<ToggleGroupComponents.Root
|
<ToggleGroupComponents.Root
|
||||||
className="inline-flex bg-slate-800 rounded shadow-[0_2px_10px] shadow-black space-x-px"
|
className="inline-flex my-auto bg-slate-800 h-fit rounded shadow-[0_2px_10px] shadow-black space-x-px"
|
||||||
type="single"
|
type="single"
|
||||||
defaultValue="center"
|
defaultValue="center"
|
||||||
aria-label="Text alignment"
|
aria-label="Text alignment"
|
||||||
|
@ -246,7 +246,6 @@ function buildStaggeredText(
|
|||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Spring",
|
type: "Spring",
|
||||||
damping: 15,
|
damping: 15,
|
||||||
@ -370,7 +369,7 @@ function buildStaggeredText(
|
|||||||
|
|
||||||
export const EXAMPLE_ANIMATED_ENTITIES: Array<z.input<typeof AnimatedEntity>> =
|
export const EXAMPLE_ANIMATED_ENTITIES: Array<z.input<typeof AnimatedEntity>> =
|
||||||
[
|
[
|
||||||
buildStaggeredText("Ehrenmann?", 2.0, {
|
buildStaggeredText("Work in Progress...", 2.0, {
|
||||||
value: [255, 255, 255, 1.0],
|
value: [255, 255, 255, 1.0],
|
||||||
}),
|
}),
|
||||||
// buildText("Wie gehts?", 2.5, 40, 40, { value: [200, 200, 200, 1.0] }),
|
// buildText("Wie gehts?", 2.5, 40, 40, { value: [200, 200, 200, 1.0] }),
|
||||||
|
@ -4,13 +4,19 @@ import { useRenderStateStore } from "stores/render-state.store";
|
|||||||
|
|
||||||
export default function useKeyControls() {
|
export default function useKeyControls() {
|
||||||
const handleKeyPress = useCallback((e: KeyboardEvent) => {
|
const handleKeyPress = useCallback((e: KeyboardEvent) => {
|
||||||
if (e.code === "Space") {
|
// Only run shortcuts if no input is focused
|
||||||
useRenderStateStore.getState().togglePlaying();
|
|
||||||
}
|
if (document.activeElement?.nodeName !== "INPUT") {
|
||||||
if (e.code === "Backspace") {
|
if (e.code === "Space") {
|
||||||
const selectedEntity = useEntitiesStore.getState().selectedEntity;
|
e.preventDefault();
|
||||||
if (selectedEntity !== undefined) {
|
useRenderStateStore.getState().togglePlaying();
|
||||||
useEntitiesStore.getState().deleteEntity(selectedEntity);
|
}
|
||||||
|
if (e.code === "Backspace") {
|
||||||
|
const selectedEntity = useEntitiesStore.getState().selectedEntity;
|
||||||
|
|
||||||
|
if (selectedEntity !== undefined) {
|
||||||
|
useEntitiesStore.getState().deleteEntity(selectedEntity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -130,9 +130,9 @@ export function getAnimatedPropertiesByAnimatedEntity(
|
|||||||
label: "Origin",
|
label: "Origin",
|
||||||
});
|
});
|
||||||
animatedProperties.push({
|
animatedProperties.push({
|
||||||
propertyPath: "radius",
|
propertyPath: "size",
|
||||||
animatedValue: animatedEntity.size,
|
animatedValue: animatedEntity.size,
|
||||||
label: "Radius",
|
label: "Size",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (animatedEntity.transform) {
|
if (animatedEntity.transform) {
|
||||||
|
@ -79,3 +79,36 @@ export function flattenedKeyframesByEntity(
|
|||||||
|
|
||||||
return keyframes;
|
return keyframes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a value inside an object with its path: example: set({}, 'a.b.c', '...') => { a: { b: { c: '...' } } }
|
||||||
|
* If one of the keys in path doesn't exists in object, it'll be created.
|
||||||
|
*
|
||||||
|
* @param object Object to manipulate
|
||||||
|
* @param path Path to the object field that need to be created/updated
|
||||||
|
* @param value Value to set
|
||||||
|
*/
|
||||||
|
export function set(object: any, path: string, value: any) {
|
||||||
|
const decomposedPath = path.split(".");
|
||||||
|
const base = decomposedPath[0];
|
||||||
|
|
||||||
|
if (base === undefined) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign an empty object in order to spread object
|
||||||
|
if (!object.hasOwnProperty(base)) {
|
||||||
|
object[base] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if there is still layers to traverse
|
||||||
|
value =
|
||||||
|
decomposedPath.length <= 1
|
||||||
|
? value
|
||||||
|
: set(object[base], decomposedPath.slice(1).join("."), value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...object,
|
||||||
|
[base]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -897,6 +897,18 @@
|
|||||||
resolved "https://packages.unom.io/@tempblade%2fcommon/-/common-2.0.1.tgz#2849607543549b993a2870b6a9f590d29961d776"
|
resolved "https://packages.unom.io/@tempblade%2fcommon/-/common-2.0.1.tgz#2849607543549b993a2870b6a9f590d29961d776"
|
||||||
integrity sha512-8uCqsfu2tcQq4O4XODS7Hn7Mj9hZh+Rh+Y0Fsej9Bbemn/WwlIT0WrUSzWGMZLcTspvgl6kz/ljBzCqLAa3Yyw==
|
integrity sha512-8uCqsfu2tcQq4O4XODS7Hn7Mj9hZh+Rh+Y0Fsej9Bbemn/WwlIT0WrUSzWGMZLcTspvgl6kz/ljBzCqLAa3Yyw==
|
||||||
|
|
||||||
|
"@types/lodash.set@^4.3.7":
|
||||||
|
version "4.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash.set/-/lodash.set-4.3.7.tgz#784fccea3fbef4d0949d1897a780f592da700942"
|
||||||
|
integrity sha512-bS5Wkg/nrT82YUfkNYPSccFrNZRL+irl7Yt4iM6OTSQ0VZJED2oUIVm15NkNtUAQ8SRhCe+axqERUV6MJgkeEg==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash@*":
|
||||||
|
version "4.14.195"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632"
|
||||||
|
integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==
|
||||||
|
|
||||||
"@types/node@^18.7.10":
|
"@types/node@^18.7.10":
|
||||||
version "18.16.16"
|
version "18.16.16"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.16.tgz#3b64862856c7874ccf7439e6bab872d245c86d8e"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.16.tgz#3b64862856c7874ccf7439e6bab872d245c86d8e"
|
||||||
@ -1393,6 +1405,11 @@ lines-and-columns@^1.1.6:
|
|||||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||||
|
|
||||||
|
lodash.set@^4.3.2:
|
||||||
|
version "4.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
|
||||||
|
integrity sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==
|
||||||
|
|
||||||
loose-envify@^1.0.0, loose-envify@^1.1.0:
|
loose-envify@^1.0.0, loose-envify@^1.1.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user