diff --git a/app/package.json b/app/package.json
index ff55051..45e4bf3 100644
--- a/app/package.json
+++ b/app/package.json
@@ -19,10 +19,12 @@
"@radix-ui/react-toolbar": "^1.0.3",
"@tauri-apps/api": "^1.3.0",
"@tempblade/common": "^2.0.1",
+ "@types/lodash.set": "^4.3.7",
"@unom/style": "^0.2.14",
"canvaskit-wasm": "^0.38.1",
"framer-motion": "^10.12.12",
"immer": "^10.0.2",
+ "lodash.set": "^4.3.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"uuid": "^9.0.0",
diff --git a/app/src/App.tsx b/app/src/App.tsx
index 7a4ddb8..c26161c 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -27,8 +27,8 @@ export default function App() {
-
-
+
+
diff --git a/app/src/components/Canvas/index.tsx b/app/src/components/Canvas/index.tsx
index 15fc0ab..6996d5d 100644
--- a/app/src/components/Canvas/index.tsx
+++ b/app/src/components/Canvas/index.tsx
@@ -26,18 +26,17 @@ const CanvasComponent: FC = () => {
}, []);
return (
-
-
-
-
+
+
);
};
diff --git a/app/src/components/Timeline/KeyframeIndicator.tsx b/app/src/components/Timeline/KeyframeIndicator.tsx
index b0f5a8e..a558385 100644
--- a/app/src/components/Timeline/KeyframeIndicator.tsx
+++ b/app/src/components/Timeline/KeyframeIndicator.tsx
@@ -1,51 +1,111 @@
import { ease } from "@unom/style";
-import { motion } from "framer-motion";
+import { PanInfo, motion } from "framer-motion";
import { AnimationData } from "primitives/AnimatedEntities";
import { Keyframe } from "primitives/Keyframe";
-import { FC } from "react";
+import { FC, useCallback, useState } from "react";
import { z } from "zod";
-import { TIMELINE_SCALE } from "./common";
+import { TIMELINE_SCALE, calculateOffset } from "./common";
import { AnimatedNumber, AnimatedVec2, AnimatedVec3 } from "primitives/Values";
import { useKeyframeStore } from "stores/keyframe.store";
+import { produce } from "immer";
const KeyframeIndicator: FC<{
keyframe: z.input
;
animationData: z.input;
-}> = ({ keyframe, animationData }) => {
+ onUpdate?: (e: z.input) => void;
+}> = ({ keyframe, animationData, onUpdate }) => {
const { selectedKeyframe, selectKeyframe, deselectKeyframe } =
useKeyframeStore();
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 (
e.preventDefault()}
+ variants={{
+ enter: {},
+ from: {},
+ exit: {},
+ tap: {},
+ drag: {},
+ }}
data-selected={selected}
+ onDragStart={() => setIsDragged(true)}
+ onDragEnd={(e, info) => {
+ e.preventDefault();
+ setIsDragged(false);
+ if (onUpdate) {
+ handleUpdate(info);
+ }
+ }}
+ onMouseDown={(e) => e.preventDefault()}
dragConstraints={{ left: 0 }}
+ initial={{
+ x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
+ scale: 0,
+ }}
+ whileTap={{ scale: 1.1 }}
animate={{
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
+ scale: 1,
}}
transition={ease.quint(0.4).out}
- style={{
- clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
+ onClick={() => {
+ if (!isDragged) {
+ selected ? deselectKeyframe() : selectKeyframe(keyframe.id);
+ }
}}
- onClick={() =>
- selected ? deselectKeyframe() : selectKeyframe(keyframe.id)
- }
- className="bg-indigo-500 data-[selected=true]:bg-indigo-300 absolute w-2 h-2 z-30 select-none"
- />
+ className="h-full absolute z-30 select-none w-3 flex items-center justify-center filter
+ data-[selected=true]:drop-shadow-[0px_2px_6px_rgba(255,255,255,1)] transition-colors"
+ >
+
+
);
};
const AnimatedNumberKeyframeIndicator: FC<{
animatedNumber: z.input;
animationData: z.input;
-}> = ({ animatedNumber, animationData }) => {
+ onUpdate: (e: z.input) => void;
+}> = ({ animatedNumber, animationData, onUpdate }) => {
return (
<>
- {animatedNumber.keyframes.values.map((keyframe) => (
+ {animatedNumber.keyframes.values.map((keyframe, index) => (
+ onUpdate(
+ produce(animatedNumber, (draft) => {
+ draft.keyframes.values[index] = keyframe;
+ })
+ )
+ }
key={keyframe.id}
keyframe={keyframe}
animationData={animationData}
@@ -65,11 +125,29 @@ const AnimatedVec2KeyframeIndicator: FC<{
animatedVec2: z.input;
dimension?: DimensionsVec2;
animationData: z.input;
-}> = ({ animatedVec2, animationData, dimension }) => {
+ onUpdate: (e: z.input) => void;
+}> = ({ animatedVec2, animationData, dimension, onUpdate }) => {
+ const handleUpdate = useCallback(
+ (
+ animatedNumber: z.input,
+ dimensionIndex: number
+ ) => {
+ onUpdate(
+ produce(animatedVec2, (draft) => {
+ draft.keyframes[dimensionIndex] = animatedNumber;
+ })
+ );
+ },
+ [animatedVec2]
+ );
+
if (dimension) {
return (
+ handleUpdate(animatedNumber, VEC2_DIMENSION_INDEX_MAPPING[dimension])
+ }
animatedNumber={
animatedVec2.keyframes[VEC2_DIMENSION_INDEX_MAPPING[dimension]]
}
@@ -81,6 +159,7 @@ const AnimatedVec2KeyframeIndicator: FC<{
<>
{animatedVec2.keyframes.map((animatedNumber, index) => (
handleUpdate(animatedNumber, index)}
key={index}
animatedNumber={animatedNumber}
animationData={animationData}
@@ -101,11 +180,29 @@ const AnimatedVec3KeyframeIndicator: FC<{
animatedVec3: z.input;
animationData: z.input;
dimension?: DimensionsVec3;
-}> = ({ animatedVec3, animationData, dimension }) => {
+ onUpdate: (e: z.input) => void;
+}> = ({ animatedVec3, animationData, dimension, onUpdate }) => {
+ const handleUpdate = useCallback(
+ (
+ animatedNumber: z.input,
+ dimensionIndex: number
+ ) => {
+ onUpdate(
+ produce(animatedVec3, (draft) => {
+ draft.keyframes[dimensionIndex] = animatedNumber;
+ })
+ );
+ },
+ [animatedVec3]
+ );
+
if (dimension) {
return (
+ handleUpdate(animatedNumber, VEC3_DIMENSION_INDEX_MAPPING[dimension])
+ }
animatedNumber={
animatedVec3.keyframes[VEC3_DIMENSION_INDEX_MAPPING[dimension]]
}
@@ -118,6 +215,7 @@ const AnimatedVec3KeyframeIndicator: FC<{
{animatedVec3.keyframes.map((animatedNumber, index) => (
handleUpdate(animatedNumber, index)}
animatedNumber={animatedNumber}
animationData={animationData}
/>
diff --git a/app/src/components/Timeline/Track.tsx b/app/src/components/Timeline/Track.tsx
index ff88080..1c5eb19 100644
--- a/app/src/components/Timeline/Track.tsx
+++ b/app/src/components/Timeline/Track.tsx
@@ -6,10 +6,7 @@ import { FC, useState } from "react";
import { useEntitiesStore } from "stores/entities.store";
import { z } from "zod";
import { shallow } from "zustand/shallow";
-import KeyframeIndicator, {
- AnimatedVec2KeyframeIndicator,
- AnimatedVec3KeyframeIndicator,
-} from "./KeyframeIndicator";
+import KeyframeIndicator from "./KeyframeIndicator";
import { TIMELINE_SCALE, calculateOffset } from "./common";
import { TriangleDownIcon } from "@radix-ui/react-icons";
import TrackPropertiesEditor from "./TrackPropertiesEditor";
@@ -49,9 +46,14 @@ const Track: FC = ({
value={entity}
dragListener={false}
dragControls={controls}
+ onMouseDown={(e) => e.preventDefault()}
className="min-h-8 relative flex flex-1 flex-col gap-1 select-none"
>
-
+
e.preventDefault()}
+ className="flex flex-row gap-1 select-none"
+ >
e.preventDefault()}
onPointerDown={(e) => controls.start(e)}
@@ -132,10 +134,10 @@ const Track: FC = ({
},
});
}}
- 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"
/>
e.preventDefault()}
drag="x"
animate={{
@@ -194,10 +196,10 @@ const Track: FC = ({
},
});
}}
- 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"
>
-
+
{isExpanded && }
);
diff --git a/app/src/components/Timeline/TrackPropertiesEditor.tsx b/app/src/components/Timeline/TrackPropertiesEditor.tsx
index 7811bad..fb3eb20 100644
--- a/app/src/components/Timeline/TrackPropertiesEditor.tsx
+++ b/app/src/components/Timeline/TrackPropertiesEditor.tsx
@@ -5,7 +5,7 @@ import {
} from "primitives/AnimatedEntities";
import { AnimatedProperty } from "primitives/AnimatedProperty";
import { AnimatedVec2, ValueType } from "primitives/Values";
-import { FC, useMemo, useState } from "react";
+import { FC, useCallback, useMemo, useState } from "react";
import { z } from "zod";
import {
AnimatedNumberKeyframeIndicator,
@@ -13,18 +13,34 @@ import {
AnimatedVec3KeyframeIndicator,
} 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";
const TrackAnimatedPropertyKeyframes: FC<{
animatedProperty: z.input;
animationData: z.input;
+ onUpdate: (animatedProperty: z.input) => void;
selectedDimension?: "x" | "y" | "z";
-}> = ({ animatedProperty, animationData, selectedDimension }) => {
+}> = ({ animatedProperty, animationData, selectedDimension, onUpdate }) => {
+ const handleUpdate = useCallback(
+ (animatedValue: z.input) => {
+ onUpdate({ ...animatedProperty, animatedValue });
+ },
+ [onUpdate, animatedProperty]
+ );
+
switch (animatedProperty.animatedValue.type) {
case "Number":
return (
);
case "Vec2":
@@ -33,6 +49,7 @@ const TrackAnimatedPropertyKeyframes: FC<{
dimension={selectedDimension !== "z" ? selectedDimension : undefined}
animatedVec2={animatedProperty.animatedValue}
animationData={animationData}
+ onUpdate={handleUpdate}
/>
);
@@ -42,6 +59,7 @@ const TrackAnimatedPropertyKeyframes: FC<{
dimension={selectedDimension}
animatedVec3={animatedProperty.animatedValue}
animationData={animationData}
+ onUpdate={handleUpdate}
/>
);
default:
@@ -52,14 +70,18 @@ const TrackAnimatedPropertyKeyframes: FC<{
const TrackAnimatedProperty: FC<{
animatedProperty: z.input;
animationData: z.input;
- trackIndex: number;
-}> = ({ animatedProperty, animationData }) => {
+ onUpdate: (e: z.input) => void;
+}> = ({ animatedProperty, animationData, onUpdate }) => {
const [selectedDimension, setSelectedDimension] = useState<"x" | "y" | "z">();
return (
-
-
-
{animatedProperty.label}
+
+
+
{animatedProperty.label}
setSelectedDimension("x")}
@@ -90,36 +112,59 @@ const TrackAnimatedProperty: FC<{
? selectedDimension
: undefined
}
+ onUpdate={onUpdate}
animatedProperty={animatedProperty}
animationData={animationData}
/>
-
+
);
};
-const TrackPropertiesEditor: FC<{ entity: z.input
}> = ({
- entity,
-}) => {
+const TrackPropertiesEditor: FC<{
+ entity: z.input;
+}> = ({ entity }) => {
const animatedProperties = useMemo(
() => getAnimatedPropertiesByAnimatedEntity(entity),
[entity]
);
+ const handleUpdate = useCallback(
+ (animatedProperty: z.input) => {
+ 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 (
-
+
{animatedProperties.map((animatedProperty, index) => (
))}
-
+
);
};
export default TrackPropertiesEditor;
-
-AnimatedVec2._def.typeName;
diff --git a/app/src/components/Timeline/index.tsx b/app/src/components/Timeline/index.tsx
index 61e1110..a971299 100644
--- a/app/src/components/Timeline/index.tsx
+++ b/app/src/components/Timeline/index.tsx
@@ -26,7 +26,7 @@ const Timeline: FC = () => {
}));
return (
-
+
-
+
diff --git a/app/src/components/ToggleGroup.tsx b/app/src/components/ToggleGroup.tsx
index 92ee6aa..e649da8 100644
--- a/app/src/components/ToggleGroup.tsx
+++ b/app/src/components/ToggleGroup.tsx
@@ -1,5 +1,6 @@
import { FC, ReactNode } from "react";
import * as ToggleGroupComponents from "@radix-ui/react-toggle-group";
+import { motion } from "framer-motion";
const ToggleGroupItem: FC<{
children: ReactNode;
@@ -9,23 +10,26 @@ const ToggleGroupItem: FC<{
return (
- {children}
+
+ {children}
+
);
};
const ToggleGroup: FC<{ children: ReactNode }> = ({ children }) => (
> =
[
- buildStaggeredText("Ehrenmann?", 2.0, {
+ buildStaggeredText("Work in Progress...", 2.0, {
value: [255, 255, 255, 1.0],
}),
// buildText("Wie gehts?", 2.5, 40, 40, { value: [200, 200, 200, 1.0] }),
diff --git a/app/src/hooks/useKeyControls.ts b/app/src/hooks/useKeyControls.ts
index 33700b5..88f6b07 100644
--- a/app/src/hooks/useKeyControls.ts
+++ b/app/src/hooks/useKeyControls.ts
@@ -4,13 +4,19 @@ import { useRenderStateStore } from "stores/render-state.store";
export default function useKeyControls() {
const handleKeyPress = useCallback((e: KeyboardEvent) => {
- if (e.code === "Space") {
- useRenderStateStore.getState().togglePlaying();
- }
- if (e.code === "Backspace") {
- const selectedEntity = useEntitiesStore.getState().selectedEntity;
- if (selectedEntity !== undefined) {
- useEntitiesStore.getState().deleteEntity(selectedEntity);
+ // Only run shortcuts if no input is focused
+
+ if (document.activeElement?.nodeName !== "INPUT") {
+ if (e.code === "Space") {
+ e.preventDefault();
+ useRenderStateStore.getState().togglePlaying();
+ }
+ if (e.code === "Backspace") {
+ const selectedEntity = useEntitiesStore.getState().selectedEntity;
+
+ if (selectedEntity !== undefined) {
+ useEntitiesStore.getState().deleteEntity(selectedEntity);
+ }
}
}
}, []);
diff --git a/app/src/primitives/AnimatedEntities.ts b/app/src/primitives/AnimatedEntities.ts
index 7b9dd06..5c6be5e 100644
--- a/app/src/primitives/AnimatedEntities.ts
+++ b/app/src/primitives/AnimatedEntities.ts
@@ -130,9 +130,9 @@ export function getAnimatedPropertiesByAnimatedEntity(
label: "Origin",
});
animatedProperties.push({
- propertyPath: "radius",
+ propertyPath: "size",
animatedValue: animatedEntity.size,
- label: "Radius",
+ label: "Size",
});
if (animatedEntity.transform) {
diff --git a/app/src/utils/index.ts b/app/src/utils/index.ts
index 9ec0cca..771b8cb 100644
--- a/app/src/utils/index.ts
+++ b/app/src/utils/index.ts
@@ -79,3 +79,36 @@ export function flattenedKeyframesByEntity(
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,
+ };
+}
diff --git a/app/yarn.lock b/app/yarn.lock
index 2562e6a..3906dba 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -897,6 +897,18 @@
resolved "https://packages.unom.io/@tempblade%2fcommon/-/common-2.0.1.tgz#2849607543549b993a2870b6a9f590d29961d776"
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":
version "18.16.16"
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"
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:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"