improve forms by adding select and float inputs

performance fix
This commit is contained in:
2023-06-01 01:24:42 +02:00
parent ebb2408a68
commit fcd3afa3f2
21 changed files with 872 additions and 345 deletions

View File

@@ -4,39 +4,35 @@ import Canvas from "./components/Canvas";
import Properties, { PropertiesContainer } from "components/Properties";
import MenuBar from "components/MenuBar";
import ToolBar from "components/ToolBar";
import { useEffect } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { useFontsStore } from "stores/fonts.store";
import useKeyControls from "hooks/useKeyControls";
import { useFontsStore } from "stores/fonts.store";
export default function App() {
const { setFonts } = useFontsStore();
const fontsStoreDidInit = useFontsStore((store) => store.didInit);
useKeyControls();
useEffect(() => {
invoke("get_system_families").then((data) => {
if (data && Array.isArray(data)) {
setFonts(data);
}
});
}, []);
return (
<div className="bg-gray-950 overflow-y-hidden h-full w-full flex flex-col">
<div className="bg-gray-950 h-full w-full flex flex-col overflow-hidden">
<MenuBar />
<div className="flex flex-row w-full h-full">
<div className="flex flex-row flex-[1] overflow-hidden">
<ToolBar />
<div className="flex flex-col ml-4 mr-4 mt-4 h-full w-full overflow-y-auto">
<div className="flex w-full gap-4 flex-col lg:flex-row mb-4 justify-center items-center">
<Canvas />
<PropertiesContainer>
<Properties />
</PropertiesContainer>
{fontsStoreDidInit && (
<div className="flex flex-col pl-4 gap-4 pr-4 overflow-x-hidden overflow-y-auto">
<div className="flex w-full gap-4 flex-col lg:flex-row justify-center items-center">
<Canvas />
<PropertiesContainer>
<Properties />
</PropertiesContainer>
</div>
<Timeline />
</div>
<Timeline />
</div>
)}
</div>
</div>
);
}
{
/* */
}

View File

@@ -1,9 +1,5 @@
import { FC, useMemo } from "react";
import { useEffect, useRef, useState } from "react";
import { useTimelineStore } from "stores/timeline.store";
import { useRenderStateStore } from "stores/render-state.store";
import { useEntitiesStore } from "stores/entities.store";
import { Drawer } from "drawers/draw";
import { PlaybackService } from "services/playback.service";
type CanvasProps = {};

View File

@@ -0,0 +1,30 @@
import { useCallback, useState } from "react";
import { z } from "zod";
type FloatInputProps = {
value: number;
onChange: (value: number) => void;
id: string;
};
const FloatInput: React.FC<FloatInputProps> = ({ value, onChange, id }) => {
const [inputValue, setInputValue] = useState<string>(value.toString());
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value.replace(",", ".");
setInputValue(val);
const nextValue = z.coerce.number().min(-9999).max(9999).safeParse(val);
if (nextValue.success) {
onChange(nextValue.data);
}
},
[setInputValue, onChange]
);
return <input id={id} onChange={handleInputChange} value={inputValue} />;
};
export default FloatInput;

View File

@@ -0,0 +1,110 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
import { cn } from "utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<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 ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-slate-900 text-popover-foreground shadow-md animate-in fade-in-80",
className
)}
{...props}
>
<SelectPrimitive.Viewport className={cn("p-1")}>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
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-indigo-800 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-slate-800", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
};

View File

@@ -1,11 +1,24 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "utils";
import { Cross2Icon } from "@radix-ui/react-icons";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverArrow = PopoverPrimitive.Arrow;
const PopoverClose: React.FC<{ onClick?: () => void }> = ({ onClick }) => (
<PopoverPrimitive.Close
onClick={onClick}
className="rounded-full h-[25px] w-[25px] inline-flex items-center justify-center text-white absolute top-[5px] right-[5px] hover:bg-indigo-600 focus:shadow-[0_0_0_2px] focus:shadow-indigo-500 outline-none cursor-default"
aria-label="Close"
>
<Cross2Icon />
</PopoverPrimitive.Close>
);
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
@@ -25,4 +38,4 @@ const PopoverContent = React.forwardRef<
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };
export { Popover, PopoverTrigger, PopoverClose, PopoverContent, PopoverArrow };

View File

@@ -12,6 +12,13 @@ import { z } from "zod";
import { AnimatedVec2Properties, ColorProperties } from "./Values";
import { PropertiesProps } from "./common";
import { useFontsStore } from "stores/fonts.store";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "components/Inputs/Select";
type TextPropertiesProps = PropertiesProps<z.input<typeof AnimatedTextEntity>>;
type StaggeredTextPropertiesProps = PropertiesProps<
@@ -29,13 +36,17 @@ export const PaintProperties: FC<PaintPropertiesProps> = ({
}) => {
return (
<div>
<label className="flex flex-col items-start">
<span className="label">PaintStyle</span>
<select
value={entity.style.type}
onChange={(e) => {
if (entity.style.type !== e.target.value) {
const paintStyle = { type: e.target.value };
<fieldset>
<label htmlFor="staggered-text-letter-font">Font</label>
</fieldset>
<fieldset>
<label htmlFor="paint-style-type">PaintStyle</label>
<Select
defaultValue={entity.style.type}
onValueChange={(value) => {
if (entity.style.type !== value) {
const paintStyle = { type: value };
const parsedPaintStyle = PaintStyle.parse(paintStyle);
@@ -43,13 +54,16 @@ export const PaintProperties: FC<PaintPropertiesProps> = ({
}
}}
>
{Object.keys(PaintStyleType.Values).map((key) => (
<option key={key} value={key}>
{key}
</option>
))}
</select>
</label>
<SelectTrigger>
<SelectValue placeholder="Choose a paint style" />
</SelectTrigger>
<SelectContent className="overflow-hidden">
{Object.keys(PaintStyleType.Values).map((paintStyleType) => (
<SelectItem value={paintStyleType}>{paintStyleType}</SelectItem>
))}
</SelectContent>
</Select>
</fieldset>
{entity.style.color && (
<ColorProperties
label="Color"
@@ -76,16 +90,18 @@ export const TextProperties: FC<TextPropertiesProps> = ({
initial="from"
transition={ease.quint(0.9).out}
>
<label className="flex flex-col items-start">
<span className="label">Text</span>
<fieldset>
<label htmlFor="text-content">Text</label>
<input
id="text-content"
value={entity.text}
onChange={(e) => onUpdate({ ...entity, text: e.target.value })}
/>
</label>
<label className="flex flex-col items-start">
<span className="label">Size</span>
</fieldset>
<fieldset>
<label htmlFor="text-size">Size</label>
<input
id="text-size"
value={entity.paint.size}
onChange={(e) =>
onUpdate({
@@ -93,40 +109,31 @@ export const TextProperties: FC<TextPropertiesProps> = ({
paint: { ...entity.paint, size: Number(e.target.value) },
})
}
></input>
</label>
<label className="flex flex-col items-start">
<span className="label">Font</span>
<select
onChange={(e) =>
/>
</fieldset>
<fieldset>
<label htmlFor="text-font">Font</label>
<Select
defaultValue={entity.paint.font_name}
onValueChange={(val) => {
onUpdate({
...entity,
cache: { valid: false },
paint: { ...entity.paint, font_name: e.target.value },
})
}
value={entity.paint.font_name}
paint: { ...entity.paint, font_name: val },
});
}}
>
{fonts.map((font) => (
<option value={font} key={font}>
{font}
</option>
))}
</select>
</label>
<label>
<span className="label">Size</span>
<input
value={entity.paint.size}
onChange={(e) =>
onUpdate({
...entity,
cache: { valid: false },
paint: { ...entity.paint, size: Number(e.target.value) },
})
}
></input>
</label>
<SelectTrigger>
<SelectValue placeholder="Choose a font" />
</SelectTrigger>
<SelectContent>
{fonts.map((font) => (
<SelectItem value={font}>{font}</SelectItem>
))}
</SelectContent>
</Select>
</fieldset>
</motion.div>
);
};
@@ -144,9 +151,10 @@ export const StaggeredTextProperties: FC<StaggeredTextPropertiesProps> = ({
initial="from"
transition={ease.quint(0.9).out}
>
<label className="flex flex-col items-start">
<span className="label">Text</span>
<fieldset>
<label htmlFor="staggered-text-content">Text</label>
<input
id="staggered-text-content"
value={entity.text}
onChange={(e) =>
onUpdate({
@@ -156,32 +164,36 @@ export const StaggeredTextProperties: FC<StaggeredTextPropertiesProps> = ({
})
}
/>
</label>
<label className="flex flex-col items-start">
<span className="label">Font</span>
<select
onChange={(e) => {
</fieldset>
<fieldset>
<label htmlFor="staggered-text-letter-font">Font</label>
<Select
defaultValue={entity.letter.paint.font_name}
onValueChange={(val) => {
onUpdate({
...entity,
cache: { valid: false },
letter: {
...entity.letter,
paint: { ...entity.letter.paint, font_name: e.target.value },
paint: { ...entity.letter.paint, font_name: val },
},
});
}}
value={entity.letter.paint.font_name}
>
{fonts.map((font) => (
<option value={font} key={font}>
{font}
</option>
))}
</select>
</label>
<label className="flex flex-col items-start">
<span className="label">Size</span>
<SelectTrigger>
<SelectValue placeholder="Choose a font" />
</SelectTrigger>
<SelectContent className="overflow-hidden">
{fonts.map((font) => (
<SelectItem value={font}>{font}</SelectItem>
))}
</SelectContent>
</Select>
</fieldset>
<fieldset>
<label htmlFor="staggered-text-letter-size">Size</label>
<input
id="staggered-text-letter-size"
value={entity.letter.paint.size}
onChange={(e) =>
onUpdate({
@@ -196,8 +208,8 @@ export const StaggeredTextProperties: FC<StaggeredTextPropertiesProps> = ({
},
})
}
></input>
</label>
/>
</fieldset>
<PaintProperties
entity={entity.letter.paint}
onUpdate={(paint) =>

View File

@@ -3,15 +3,96 @@ import { PropertiesProps } from "./common";
import { FC } from "react";
import { z } from "zod";
import { produce } from "immer";
import { Interpolation } from "primitives/Interpolation";
import { Interpolation, InterpolationType } from "primitives/Interpolation";
import { Color } from "primitives/Paint";
import { colorToString, parseColor, parseCssColor } from "@tempblade/common";
import { parseCssColor } from "@tempblade/common";
import { rgbToHex } from "utils";
import { SpringInterpolation } from "primitives/Interpolation";
import FloatInput from "components/Inputs/FloatInput";
import { Keyframe } from "primitives/Keyframe";
const InterpolationProperties: FC<
const SpringInterpolationProperties: FC<
PropertiesProps<z.input<typeof SpringInterpolation>>
> = ({ entity, onUpdate }) => {
return <div></div>;
};
export const InterpolationProperties: FC<
PropertiesProps<z.input<typeof Interpolation>>
> = ({ entity, onUpdate }) => {
return <div>Interpolation: {entity.type}</div>;
return (
<fieldset>
<label className="label" htmlFor="interpolation-type">
Interpolation Type
</label>
<select
id="interpolation-type"
onChange={(e) => {
onUpdate({
...entity,
type: e.target.value as any,
});
}}
value={entity.type}
>
{Object.keys(InterpolationType.Values).map((key) => (
<option key={key} value={key}>
{key}
</option>
))}
</select>
</fieldset>
);
};
export const KeyframeProperties: FC<
PropertiesProps<z.input<typeof Keyframe>>
> = ({ entity, onUpdate }) => {
return (
<>
<fieldset>
<label htmlFor="keyframe-offset">Offset</label>
<FloatInput
value={entity.offset}
onChange={(value) =>
onUpdate(
produce(entity, (draft) => {
draft.offset = value;
})
)
}
id="keyframe-offset"
/>
</fieldset>
<fieldset>
<label>Value</label>
<FloatInput
value={entity.value}
onChange={(value) =>
onUpdate(
produce(entity, (draft) => {
draft.value = value;
})
)
}
id="keyframe-value"
/>
</fieldset>
{entity.interpolation && (
<InterpolationProperties
onUpdate={(updatedEntity) =>
onUpdate(
produce(entity, (draft) => {
draft.interpolation = updatedEntity;
})
)
}
entity={entity.interpolation}
/>
)}
</>
);
};
const AnimatedNumberProperties: FC<
@@ -23,51 +104,16 @@ const AnimatedNumberProperties: FC<
{entity.keyframes.values.map((keyframe, index) => {
return (
<div key={index}>
<div className="flex flex-row gap-3">
<label className="flex flex-col items-start w-16">
<span className="label text-sm opacity-70">Offset</span>
<input
value={keyframe.offset}
onChange={(e) =>
onUpdate(
produce(entity, (draft) => {
draft.keyframes.values[index].offset = Number(
e.target.value
);
})
)
}
/>
</label>
<label className="flex flex-col items-start">
<span className="label text-sm opacity-70">Value</span>
<input
value={keyframe.value}
onChange={(e) =>
onUpdate(
produce(entity, (draft) => {
draft.keyframes.values[index].value = Number(
e.target.value
);
})
)
}
/>
</label>
</div>
{keyframe.interpolation && (
<InterpolationProperties
onUpdate={(updatedEntity) =>
onUpdate(
produce(entity, (draft) => {
draft.keyframes.values[index].interpolation =
updatedEntity;
})
)
}
entity={keyframe.interpolation}
/>
)}
<KeyframeProperties
entity={keyframe}
onUpdate={(nextKeyframe) =>
onUpdate(
produce(entity, (draft) => {
draft.keyframes.values[index] = nextKeyframe;
})
)
}
/>
</div>
);
})}

View File

@@ -9,6 +9,7 @@ import { AnimatedNumber, AnimatedVec2, AnimatedVec3 } from "primitives/Values";
import { useKeyframeStore } from "stores/keyframe.store";
import { produce } from "immer";
import KeyframePopover from "./KeyframePopover";
import { Popover, PopoverContent, PopoverTrigger } from "components/Popover";
const KeyframeIndicator: FC<{
keyframe: z.input<typeof Keyframe>;
@@ -51,66 +52,70 @@ const KeyframeIndicator: FC<{
return (
<>
<motion.div
drag="x"
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.6,
}}
animate={{
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
scale: 1,
}}
transition={ease.quint(0.4).out}
onClick={() => {
if (isDragged) {
if (!selected) selectKeyframe(keyframe.id);
} else {
selected ? deselectKeyframe() : selectKeyframe(keyframe.id);
}
}}
className="h-full absolute z-30 select-none w-3 flex items-center justify-center filter
<Popover modal={false} open={selected}>
<PopoverTrigger asChild>
<motion.div
drag="x"
variants={{
enter: {},
from: {},
exit: {},
tap: {},
drag: {},
}}
data-selected={selected}
onDragStart={() => setIsDragged(true)}
onDragEnd={(e, info) => {
e.preventDefault();
setIsDragged(false);
if (onUpdate) {
handleUpdate(info);
}
}}
dragConstraints={{ left: 0 }}
initial={{
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 2,
scale: 0,
}}
whileTap={{
scale: 1.6,
}}
animate={{
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 2,
scale: 1,
}}
transition={ease.quint(0.4).out}
onClick={() => {
if (isDragged) {
if (!selected) selectKeyframe(keyframe.id);
} else {
selected ? deselectKeyframe() : selectKeyframe(keyframe.id);
}
}}
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(230,230,255,1)] transition-colors"
>
<KeyframePopover
onUpdate={handleValueUpdate}
keyframe={keyframe}
open={selected}
/>
<motion.span
data-selected={selected}
className="bg-gray-200
>
<motion.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>
style={{
width: 10,
height: 10,
clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
}}
/>
</motion.div>
</PopoverTrigger>
<PopoverContent className="w-80 backdrop-blur-md bg-slate-700/50">
<KeyframePopover
onClose={() => deselectKeyframe()}
onUpdate={handleValueUpdate}
keyframe={keyframe}
/>
</PopoverContent>
</Popover>
</>
);
};

View File

@@ -1,29 +1,18 @@
import { PopoverContent, PopoverPortal } from "@radix-ui/react-popover";
import { Popover } from "components/Popover";
import { PopoverClose } from "components/Popover";
import { KeyframeProperties } from "components/Properties/Values";
import { Keyframe } from "primitives/Keyframe";
import { FC } from "react";
import { z } from "zod";
const KeyframePopover: FC<{
open: boolean;
keyframe: z.input<typeof Keyframe>;
onUpdate: (k: z.input<typeof Keyframe>) => void;
}> = ({ open, keyframe, onUpdate }) => {
onClose: () => void;
}> = ({ keyframe, onUpdate, onClose }) => {
return (
<div>
<Popover open={open}>
<PopoverContent>
<label>
<span className="label">Value</span>
<input
onChange={(e) =>
onUpdate({ ...keyframe, value: Number(e.target.value) })
}
value={keyframe.value}
/>
</label>
</PopoverContent>
</Popover>
<KeyframeProperties entity={keyframe} onUpdate={onUpdate} />
<PopoverClose onClick={onClose} />
</div>
);
};

View File

@@ -1,8 +1,7 @@
import { ease } from "@unom/style";
import { useDragControls, Reorder, motion } from "framer-motion";
import { AnimationData, AnimatedEntity } from "primitives/AnimatedEntities";
import { Keyframe } from "primitives/Keyframe";
import { FC, useState } from "react";
import { FC, memo, useState, useMemo } from "react";
import { useEntitiesStore } from "stores/entities.store";
import { z } from "zod";
import { shallow } from "zustand/shallow";
@@ -10,24 +9,23 @@ 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";
type TrackProps = {
animationData: z.input<typeof AnimationData>;
name: string;
index: number;
entity: z.input<typeof AnimatedEntity>;
keyframes: Array<z.input<typeof Keyframe>>;
};
const Track: FC<TrackProps> = ({
keyframes,
animationData,
index,
name,
entity,
}) => {
const Track: FC<TrackProps> = ({ animationData, index, name, entity }) => {
const controls = useDragControls();
const flattenedKeyframes = useMemo(
() => flattenedKeyframesByEntity(entity),
[entity]
);
const [isExpanded, setIsExpanded] = useState(false);
const { updateEntity, selectEntity, selectedEntity, deselectEntity } =
@@ -91,7 +89,7 @@ const Track: FC<TrackProps> = ({
className="flex h-full flex-row relative bg-gray-900 select-none shrink-0"
>
{!isExpanded &&
keyframes.map((keyframe, index) => (
flattenedKeyframes.map((keyframe, index) => (
<KeyframeIndicator
animationData={animationData}
keyframe={keyframe}
@@ -205,4 +203,4 @@ const Track: FC<TrackProps> = ({
);
};
export default Track;
export default memo(Track);

View File

@@ -3,7 +3,6 @@ import { Reorder } from "framer-motion";
import TimePicker from "./Timepicker";
import { useEntitiesStore } from "stores/entities.store";
import Timestamp from "./Timestamp";
import { flattenedKeyframesByEntity } from "utils";
import { PauseIcon, PlayIcon } from "@radix-ui/react-icons";
import { useRenderStateStore } from "stores/render-state.store";
import Track from "./Track";
@@ -26,7 +25,7 @@ const Timeline: FC<TimelineProps> = () => {
}));
return (
<div className="flex flex-col p-4 border transition-colors focus-within:border-gray-400 border-gray-600 rounded-md">
<div className="flex flex-col grow shrink h-fit 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">
<button onClick={() => setPlaying(true)} className="w-8 h-8">
@@ -38,7 +37,7 @@ const Timeline: FC<TimelineProps> = () => {
</div>
<Timestamp />
</div>
<div className="gap-1 w-full flex flex-col overflow-x-auto">
<div className="gap-1 w-full flex flex-col overflow-x-auto overflow-y-visible">
<div className="z-20 flex flex-row gap-2">
<div className="flex-shrink-0 min-w-[200px]" />
<TimePicker />
@@ -54,7 +53,6 @@ const Timeline: FC<TimelineProps> = () => {
key={entity.id}
name={entity.type}
index={index}
keyframes={flattenedKeyframesByEntity(entity)}
animationData={entity.animation_data}
/>
))}

View File

@@ -2,6 +2,7 @@ import {
BoxIcon,
CircleIcon,
CursorArrowIcon,
FontStyleIcon,
MixIcon,
Pencil1Icon,
Pencil2Icon,
@@ -9,48 +10,81 @@ import {
TextIcon,
} from "@radix-ui/react-icons";
import * as Toolbar from "@radix-ui/react-toolbar";
import { FC, ReactNode } from "react";
import { motion } from "framer-motion";
import { FC, ReactNode, useMemo, useState } from "react";
import { EntitiesService } from "services/entities.service";
const ToolBarButton: FC<{ children: ReactNode; onClick?: () => void }> = ({
children,
onClick,
}) => {
const [didHover, setDidHover] = useState(false);
const ToolBarButton: FC<{ children: ReactNode }> = ({ children }) => {
return (
<Toolbar.Button
onClick={onClick}
onMouseOver={() => !didHover && setDidHover(true)}
asChild
className="text-white p-[10px] bg-gray-900 flex-shrink-0 flex-grow-0
basis-auto w-[40px] h-[40px] rounded inline-flex text-[13px] leading-none
items-center justify-center outline-none hover:bg-indigo-900
transition-colors
focus:relative focus:shadow-[0_0_0_2px] focus:shadow-indigo"
>
{children}
<motion.button
animate={didHover ? "enter" : undefined}
whileTap="press"
variants={{
enter: { scale: 1 },
from: { scale: 0 },
press: { scale: 0.9 },
}}
>
{children}
</motion.button>
</Toolbar.Button>
);
};
const ToolBar = () => {
const entitiesService = useMemo(() => new EntitiesService(), []);
return (
<Toolbar.Root
asChild
className="bg-gray-800 flex flex-col gap-1 p-1 h-full"
orientation="vertical"
>
<ToolBarButton>
<CursorArrowIcon width="100%" height="100%" />
</ToolBarButton>
<Toolbar.Separator />
<ToolBarButton>
<BoxIcon width="100%" height="100%" />
</ToolBarButton>
<ToolBarButton>
<CircleIcon width="100%" height="100%" />
</ToolBarButton>
<ToolBarButton>
<Pencil1Icon width="100%" height="100%" />
</ToolBarButton>
<ToolBarButton>
<MixIcon width="100%" height="100%" />
</ToolBarButton>
<Toolbar.Separator />
<ToolBarButton>
<TextIcon width="100%" height="100%" />
</ToolBarButton>
<motion.div
animate="enter"
initial="from"
transition={{ staggerChildren: 0.1 }}
variants={{ enter: {}, from: {} }}
>
<ToolBarButton>
<CursorArrowIcon width="100%" height="100%" />
</ToolBarButton>
<Toolbar.Separator />
<ToolBarButton onClick={() => entitiesService.createRect()}>
<BoxIcon width="100%" height="100%" />
</ToolBarButton>
<ToolBarButton onClick={() => entitiesService.createEllipse()}>
<CircleIcon width="100%" height="100%" />
</ToolBarButton>
<ToolBarButton>
<Pencil1Icon width="100%" height="100%" />
</ToolBarButton>
<ToolBarButton>
<MixIcon width="100%" height="100%" />
</ToolBarButton>
<Toolbar.Separator />
<ToolBarButton onClick={() => entitiesService.createText()}>
<TextIcon width="100%" height="100%" />
</ToolBarButton>
<ToolBarButton onClick={() => entitiesService.createStaggeredText()}>
<FontStyleIcon width="100%" height="100%" />
</ToolBarButton>
</motion.div>
</Toolbar.Root>
);
};

View File

@@ -2,6 +2,16 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles.css";
import { useFontsStore } from "stores/fonts.store";
import { invoke } from "@tauri-apps/api";
invoke("get_system_families").then((data) => {
if (data && Array.isArray(data)) {
const fontsStore = useFontsStore.getState();
fontsStore.setFonts(data);
fontsStore.setDidInit(true);
}
});
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>

View File

@@ -87,3 +87,18 @@ export function staticAnimatedVec3(
],
};
}
export function staticAnimatedTransform(
translate: [number, number],
scale: [number, number],
rotate: [number, number, number],
skew: [number, number]
): z.input<typeof AnimatedTransform> {
return {
type: "Transform",
translate: staticAnimatedVec2(...translate),
scale: staticAnimatedVec2(...scale),
rotate: staticAnimatedVec3(...rotate),
skew: staticAnimatedVec2(...skew),
};
}

View File

@@ -0,0 +1,124 @@
import { EntityType } from "primitives/Entities";
import { PaintStyleType, TextAlign } from "primitives/Paint";
import { staticAnimatedTransform, staticAnimatedVec2 } from "primitives/Values";
import { useEntitiesStore } from "stores/entities.store";
import { useTimelineStore } from "stores/timeline.store";
import { v4 as uuid } from "uuid";
export class EntitiesService {
get entitiesStore() {
return useEntitiesStore.getState();
}
private get timelineStore() {
return useTimelineStore.getState();
}
createUuid() {
return uuid();
}
createRect() {
return this.entitiesStore.createEntity({
type: EntityType.Enum.Rect,
id: this.createUuid(),
cache: {},
paint: {
style: {
type: PaintStyleType.Enum.Fill,
color: {
value: [233, 100, 150, 1.0],
},
},
},
size: staticAnimatedVec2(500, 500),
origin: staticAnimatedVec2(-250, -250),
position: staticAnimatedVec2(...this.timelineStore.size),
transform: staticAnimatedTransform([0, 0], [1, 1], [0, 0, 0], [0, 0]),
animation_data: {
offset: 0,
duration: 3,
},
});
}
createEllipse() {
return this.entitiesStore.createEntity({
type: EntityType.Enum.Ellipse,
id: this.createUuid(),
cache: {},
paint: {
style: {
type: PaintStyleType.Enum.Fill,
color: {
value: [233, 100, 150, 1.0],
},
},
},
radius: staticAnimatedVec2(500, 500),
origin: staticAnimatedVec2(-250, -250),
position: staticAnimatedVec2(...this.timelineStore.size),
transform: staticAnimatedTransform([0, 0], [1, 1], [0, 0, 0], [0, 0]),
animation_data: {
offset: 0,
duration: 3,
},
});
}
createText(text?: string) {
return this.entitiesStore.createEntity({
type: EntityType.Enum.Text,
id: this.createUuid(),
cache: {},
text: text || "Hallo Welt",
paint: {
align: TextAlign.Enum.Center,
size: 20,
font_name: "Helvetica-Bold",
style: {
type: PaintStyleType.Enum.Fill,
color: {
value: [233, 100, 150, 1.0],
},
},
},
origin: staticAnimatedVec2(-250, -250),
transform: staticAnimatedTransform([0, 0], [1, 1], [0, 0, 0], [0, 0]),
animation_data: {
offset: 0,
duration: 3,
},
});
}
createStaggeredText(text?: string) {
return this.entitiesStore.createEntity({
type: EntityType.Enum.StaggeredText,
id: this.createUuid(),
cache: {},
text: text || "Hallo Welt",
stagger: 0.1,
letter: {
paint: {
align: TextAlign.Enum.Center,
size: 20,
font_name: "Helvetica-Bold",
style: {
type: PaintStyleType.Enum.Fill,
color: {
value: [233, 100, 150, 1.0],
},
},
},
transform: staticAnimatedTransform([0, 0], [1, 1], [0, 0, 0], [0, 0]),
},
origin: staticAnimatedVec2(-250, -250),
transform: staticAnimatedTransform([0, 0], [1, 1], [0, 0, 0], [0, 0]),
animation_data: {
offset: 0,
duration: 3,
},
});
}
}

View File

@@ -15,7 +15,9 @@ interface EntitiesStore {
index: number,
entity: Partial<z.input<typeof AnimatedEntity>>
) => void;
createEntity: (
entity: z.input<typeof AnimatedEntity>
) => z.input<typeof AnimatedEntity>;
deleteEntity: (index: number) => void;
updateEntityById: (
id: string,
@@ -23,13 +25,20 @@ interface EntitiesStore {
) => void;
}
const useEntitiesStore = create<EntitiesStore>((set) => ({
const useEntitiesStore = create<EntitiesStore>((set, get) => ({
entities: EXAMPLE_ANIMATED_ENTITIES,
selectedKeyframe: undefined,
selectEntity: (index) => set(() => ({ selectedEntity: index })),
deselectEntity: () => set(() => ({ selectedEntity: undefined })),
selectedEntity: undefined,
setEntities: (entities) => set({ entities }),
setEntities: (entities) => {
console.log("set entities");
set({ entities });
},
createEntity: (entity) => {
set({ entities: [...get().entities, entity] });
return entity;
},
updateEntityById: (id, entity) =>
set(({ entities }) => {
const nextEntities = produce(entities, (draft) => {

View File

@@ -2,11 +2,15 @@ import { create } from "zustand";
interface FontsStore {
fonts: Array<string>;
didInit: boolean;
setDidInit: (didInit: boolean) => void;
setFonts: (fonts: Array<string>) => void;
}
const useFontsStore = create<FontsStore>((set) => ({
fonts: [],
didInit: false,
setDidInit: (didInit) => set({ didInit }),
setFonts: (fonts) => set({ fonts }),
}));

View File

@@ -13,6 +13,10 @@
@apply text-lg;
}
span {
@apply text-white;
}
h1,
h2,
h3,
@@ -122,12 +126,12 @@
}
}
.label {
@apply mb-1;
label {
@apply mb-1 text-sm opacity-70;
}
label {
@apply mb-2;
fieldset {
@apply mb-2 flex flex-col items-start w-16;
}
:root {