ui improvements

add timeline animations
This commit is contained in:
Enrico Bühler 2023-05-31 15:16:27 +02:00
parent 8d1f949280
commit e3098c4400
13 changed files with 278 additions and 73 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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] }),

View File

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

View File

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

View File

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

View File

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