mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
chore: switch from biome to eslint + prettier; fix ton of lint issues
This commit is contained in:
@@ -1,231 +0,0 @@
|
||||
import { useEditor } from "@/editor/use-editor";
|
||||
import { clamp } from "@/utils/math";
|
||||
import { NumberField } from "@/components/ui/number-field";
|
||||
import { OcCheckerboardIcon } from "@/components/icons";
|
||||
import { Fragment, useRef } from "react";
|
||||
import { useMenuPreview } from "@/editor/use-menu-preview";
|
||||
import {
|
||||
Section,
|
||||
SectionContent,
|
||||
SectionField,
|
||||
SectionHeader,
|
||||
SectionTitle,
|
||||
} from "@/components/section";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { BlendMode } from "@/rendering";
|
||||
import type { ElementType } from "@/timeline";
|
||||
import type { ElementAnimations } from "@/animation/types";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { RainDropIcon } from "@hugeicons/core-free-icons";
|
||||
import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle";
|
||||
import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property";
|
||||
import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead";
|
||||
import { resolveOpacityAtTime } from "@/animation/values";
|
||||
import { DEFAULTS } from "@/timeline/defaults";
|
||||
import { isPropertyAtDefault } from "./transform-tab";
|
||||
import type { MediaTime } from "@/wasm";
|
||||
|
||||
type BlendingElement = {
|
||||
id: string;
|
||||
opacity: number;
|
||||
type: ElementType;
|
||||
blendMode?: BlendMode;
|
||||
startTime: MediaTime;
|
||||
duration: MediaTime;
|
||||
animations?: ElementAnimations;
|
||||
};
|
||||
|
||||
const BLEND_MODE_GROUPS: { value: BlendMode; label: string }[][] = [
|
||||
[{ value: "normal", label: "Normal" }],
|
||||
[
|
||||
{ value: "darken", label: "Darken" },
|
||||
{ value: "multiply", label: "Multiply" },
|
||||
{ value: "color-burn", label: "Color Burn" },
|
||||
],
|
||||
[
|
||||
{ value: "lighten", label: "Lighten" },
|
||||
{ value: "screen", label: "Screen" },
|
||||
{ value: "plus-lighter", label: "Plus Lighter" },
|
||||
{ value: "color-dodge", label: "Color Dodge" },
|
||||
],
|
||||
[
|
||||
{ value: "overlay", label: "Overlay" },
|
||||
{ value: "soft-light", label: "Soft Light" },
|
||||
{ value: "hard-light", label: "Hard Light" },
|
||||
],
|
||||
[
|
||||
{ value: "difference", label: "Difference" },
|
||||
{ value: "exclusion", label: "Exclusion" },
|
||||
],
|
||||
[
|
||||
{ value: "hue", label: "Hue" },
|
||||
{ value: "saturation", label: "Saturation" },
|
||||
{ value: "color", label: "Color" },
|
||||
{ value: "luminosity", label: "Luminosity" },
|
||||
],
|
||||
];
|
||||
|
||||
export function BlendingTab({
|
||||
element,
|
||||
trackId,
|
||||
}: {
|
||||
element: BlendingElement;
|
||||
trackId: string;
|
||||
}) {
|
||||
const editor = useEditor();
|
||||
const isPreviewActive = useEditor((e) => e.timeline.isPreviewActive());
|
||||
const blendMode = element.blendMode ?? DEFAULTS.element.blendMode;
|
||||
const committedBlendModeRef = useRef(blendMode);
|
||||
if (!isPreviewActive) {
|
||||
committedBlendModeRef.current = blendMode;
|
||||
}
|
||||
|
||||
const {
|
||||
onPointerLeave,
|
||||
onOpenChange: handleBlendModeOpenChange,
|
||||
markCommitted,
|
||||
} = useMenuPreview();
|
||||
|
||||
const previewBlendMode = ({ value }: { value: BlendMode }) =>
|
||||
editor.timeline.previewElements({
|
||||
updates: [
|
||||
{ trackId, elementId: element.id, updates: { blendMode: value } },
|
||||
],
|
||||
});
|
||||
|
||||
const commitBlendMode = (value: BlendMode) => {
|
||||
if (editor.timeline.isPreviewActive()) {
|
||||
editor.timeline.commitPreview();
|
||||
} else {
|
||||
editor.timeline.updateElements({
|
||||
updates: [
|
||||
{
|
||||
trackId,
|
||||
elementId: element.id,
|
||||
patch: { blendMode: value },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
markCommitted();
|
||||
};
|
||||
|
||||
const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
|
||||
startTime: element.startTime,
|
||||
duration: element.duration,
|
||||
});
|
||||
const resolvedOpacity = resolveOpacityAtTime({
|
||||
baseOpacity: element.opacity,
|
||||
animations: element.animations,
|
||||
localTime,
|
||||
});
|
||||
|
||||
const opacity = useKeyframedNumberProperty({
|
||||
trackId,
|
||||
elementId: element.id,
|
||||
animations: element.animations,
|
||||
propertyPath: "opacity",
|
||||
localTime,
|
||||
isPlayheadWithinElementRange,
|
||||
displayValue: Math.round(resolvedOpacity * 100).toString(),
|
||||
parse: (input) => {
|
||||
const parsed = parseFloat(input);
|
||||
if (Number.isNaN(parsed)) return null;
|
||||
return clamp({ value: parsed, min: 0, max: 100 }) / 100;
|
||||
},
|
||||
valueAtPlayhead: resolvedOpacity,
|
||||
step: 0.01,
|
||||
buildBaseUpdates: ({ value }) => ({ opacity: value }),
|
||||
});
|
||||
|
||||
return (
|
||||
<Section collapsible sectionKey={`${element.id}:blending`}>
|
||||
<SectionHeader>
|
||||
<SectionTitle>Blending</SectionTitle>
|
||||
</SectionHeader>
|
||||
<SectionContent>
|
||||
<div className="flex items-start gap-2">
|
||||
<SectionField
|
||||
label="Opacity"
|
||||
className="w-1/2"
|
||||
beforeLabel={
|
||||
<KeyframeToggle
|
||||
isActive={opacity.isKeyframedAtTime}
|
||||
isDisabled={!isPlayheadWithinElementRange}
|
||||
title="Toggle opacity keyframe"
|
||||
onToggle={opacity.toggleKeyframe}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NumberField
|
||||
className="w-full"
|
||||
icon={
|
||||
<OcCheckerboardIcon className="size-3.5 text-muted-foreground" />
|
||||
}
|
||||
value={opacity.displayValue}
|
||||
min={0}
|
||||
max={100}
|
||||
onFocus={opacity.onFocus}
|
||||
onChange={opacity.onChange}
|
||||
onBlur={opacity.onBlur}
|
||||
onScrub={opacity.scrubTo}
|
||||
onScrubEnd={opacity.commitScrub}
|
||||
onReset={() =>
|
||||
opacity.commitValue({ value: DEFAULTS.element.opacity })
|
||||
}
|
||||
isDefault={isPropertyAtDefault({
|
||||
hasAnimatedKeyframes: opacity.hasAnimatedKeyframes,
|
||||
isPlayheadWithinElementRange,
|
||||
resolvedValue: resolvedOpacity,
|
||||
staticValue: element.opacity,
|
||||
defaultValue: DEFAULTS.element.opacity,
|
||||
})}
|
||||
dragSensitivity="slow"
|
||||
/>
|
||||
</SectionField>
|
||||
<SectionField label="Blend mode" className="w-1/2">
|
||||
<Select
|
||||
value={committedBlendModeRef.current}
|
||||
onOpenChange={handleBlendModeOpenChange}
|
||||
onValueChange={commitBlendMode}
|
||||
>
|
||||
<SelectTrigger
|
||||
icon={<HugeiconsIcon icon={RainDropIcon} />}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue placeholder="Select blend mode" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-36" onPointerLeave={onPointerLeave}>
|
||||
{BLEND_MODE_GROUPS.map((group, groupIndex) => (
|
||||
<Fragment key={group[0]?.value ?? `group-${groupIndex}`}>
|
||||
{group.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onPointerEnter={() =>
|
||||
previewBlendMode({ value: option.value })
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{groupIndex < BLEND_MODE_GROUPS.length - 1 ? (
|
||||
<SelectSeparator />
|
||||
) : null}
|
||||
</Fragment>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SectionField>
|
||||
</div>
|
||||
</SectionContent>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,462 +0,0 @@
|
||||
import { NumberField } from "@/components/ui/number-field";
|
||||
import { useEditor } from "@/editor/use-editor";
|
||||
import { clamp, isNearlyEqual } from "@/utils/math";
|
||||
import type { VisualElement } from "@/timeline";
|
||||
import {
|
||||
Section,
|
||||
SectionContent,
|
||||
SectionField,
|
||||
SectionFields,
|
||||
SectionHeader,
|
||||
SectionTitle,
|
||||
} from "@/components/section";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import {
|
||||
ArrowExpandIcon,
|
||||
Link05Icon,
|
||||
RotateClockwiseIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import {
|
||||
getGroupKeyframesAtTime,
|
||||
hasGroupKeyframeAtTime,
|
||||
} from "@/animation";
|
||||
import { resolveTransformAtTime } from "@/rendering/animation-values";
|
||||
import { DEFAULTS } from "@/timeline/defaults";
|
||||
import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead";
|
||||
import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle";
|
||||
import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property";
|
||||
import { usePropertiesStore } from "@/components/editor/panels/properties/stores/properties-store";
|
||||
|
||||
export function parseNumericInput({ input }: { input: string }): number | null {
|
||||
const parsed = parseFloat(input);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
export function isPropertyAtDefault({
|
||||
hasAnimatedKeyframes,
|
||||
isPlayheadWithinElementRange,
|
||||
resolvedValue,
|
||||
staticValue,
|
||||
defaultValue,
|
||||
}: {
|
||||
hasAnimatedKeyframes: boolean;
|
||||
isPlayheadWithinElementRange: boolean;
|
||||
resolvedValue: number;
|
||||
staticValue: number;
|
||||
defaultValue: number;
|
||||
}): boolean {
|
||||
if (hasAnimatedKeyframes && isPlayheadWithinElementRange) {
|
||||
return isNearlyEqual({
|
||||
leftValue: resolvedValue,
|
||||
rightValue: defaultValue,
|
||||
});
|
||||
}
|
||||
|
||||
return staticValue === defaultValue;
|
||||
}
|
||||
|
||||
export function TransformTab({
|
||||
element,
|
||||
trackId,
|
||||
}: {
|
||||
element: VisualElement;
|
||||
trackId: string;
|
||||
}) {
|
||||
const editor = useEditor();
|
||||
const isScaleLocked = usePropertiesStore((s) => s.isTransformScaleLocked);
|
||||
const setTransformScaleLocked = usePropertiesStore(
|
||||
(s) => s.setTransformScaleLocked,
|
||||
);
|
||||
const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
|
||||
startTime: element.startTime,
|
||||
duration: element.duration,
|
||||
});
|
||||
const resolvedTransform = resolveTransformAtTime({
|
||||
baseTransform: element.transform,
|
||||
animations: element.animations,
|
||||
localTime,
|
||||
});
|
||||
|
||||
const positionX = useKeyframedNumberProperty({
|
||||
trackId,
|
||||
elementId: element.id,
|
||||
animations: element.animations,
|
||||
propertyPath: "transform.positionX",
|
||||
localTime,
|
||||
isPlayheadWithinElementRange,
|
||||
displayValue: Math.round(resolvedTransform.position.x).toString(),
|
||||
parse: (input) => parseNumericInput({ input }),
|
||||
valueAtPlayhead: resolvedTransform.position.x,
|
||||
step: 1,
|
||||
buildBaseUpdates: ({ value }) => ({
|
||||
transform: {
|
||||
...element.transform,
|
||||
position: { ...element.transform.position, x: value },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const positionY = useKeyframedNumberProperty({
|
||||
trackId,
|
||||
elementId: element.id,
|
||||
animations: element.animations,
|
||||
propertyPath: "transform.positionY",
|
||||
localTime,
|
||||
isPlayheadWithinElementRange,
|
||||
displayValue: Math.round(resolvedTransform.position.y).toString(),
|
||||
parse: (input) => parseNumericInput({ input }),
|
||||
valueAtPlayhead: resolvedTransform.position.y,
|
||||
step: 1,
|
||||
buildBaseUpdates: ({ value }) => ({
|
||||
transform: {
|
||||
...element.transform,
|
||||
position: { ...element.transform.position, y: value },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const parseScale = (input: string) => {
|
||||
const parsed = parseNumericInput({ input });
|
||||
if (parsed === null) return null;
|
||||
return parsed / 100;
|
||||
};
|
||||
|
||||
const scaleX = useKeyframedNumberProperty({
|
||||
trackId,
|
||||
elementId: element.id,
|
||||
animations: element.animations,
|
||||
propertyPath: "transform.scaleX",
|
||||
localTime,
|
||||
isPlayheadWithinElementRange,
|
||||
displayValue: Math.round(resolvedTransform.scaleX * 100).toString(),
|
||||
parse: parseScale,
|
||||
valueAtPlayhead: resolvedTransform.scaleX,
|
||||
step: 0.01,
|
||||
buildBaseUpdates: ({ value }) => ({
|
||||
transform: {
|
||||
...element.transform,
|
||||
scaleX: value,
|
||||
...(isScaleLocked ? { scaleY: value } : {}),
|
||||
},
|
||||
}),
|
||||
buildAdditionalKeyframes: isScaleLocked
|
||||
? ({ value }) => [{ propertyPath: "transform.scaleY", value }]
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const scaleY = useKeyframedNumberProperty({
|
||||
trackId,
|
||||
elementId: element.id,
|
||||
animations: element.animations,
|
||||
propertyPath: "transform.scaleY",
|
||||
localTime,
|
||||
isPlayheadWithinElementRange,
|
||||
displayValue: Math.round(resolvedTransform.scaleY * 100).toString(),
|
||||
parse: parseScale,
|
||||
valueAtPlayhead: resolvedTransform.scaleY,
|
||||
step: 0.01,
|
||||
buildBaseUpdates: ({ value }) => ({
|
||||
transform: {
|
||||
...element.transform,
|
||||
scaleY: value,
|
||||
...(isScaleLocked ? { scaleX: value } : {}),
|
||||
},
|
||||
}),
|
||||
buildAdditionalKeyframes: isScaleLocked
|
||||
? ({ value }) => [{ propertyPath: "transform.scaleX", value }]
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const scaleFieldPropsX = {
|
||||
value: scaleX.displayValue,
|
||||
onFocus: scaleX.onFocus,
|
||||
onChange: scaleX.onChange,
|
||||
onBlur: scaleX.onBlur,
|
||||
dragSensitivity: "slow" as const,
|
||||
onScrub: scaleX.scrubTo,
|
||||
onScrubEnd: scaleX.commitScrub,
|
||||
onReset: () =>
|
||||
scaleX.commitValue({ value: DEFAULTS.element.transform.scaleX }),
|
||||
isDefault: isPropertyAtDefault({
|
||||
hasAnimatedKeyframes: scaleX.hasAnimatedKeyframes,
|
||||
isPlayheadWithinElementRange,
|
||||
resolvedValue: resolvedTransform.scaleX,
|
||||
staticValue: element.transform.scaleX,
|
||||
defaultValue: DEFAULTS.element.transform.scaleX,
|
||||
}),
|
||||
};
|
||||
|
||||
const scaleFieldPropsY = {
|
||||
value: scaleY.displayValue,
|
||||
onFocus: scaleY.onFocus,
|
||||
onChange: scaleY.onChange,
|
||||
onBlur: scaleY.onBlur,
|
||||
dragSensitivity: "slow" as const,
|
||||
onScrub: scaleY.scrubTo,
|
||||
onScrubEnd: scaleY.commitScrub,
|
||||
onReset: () =>
|
||||
scaleY.commitValue({ value: DEFAULTS.element.transform.scaleY }),
|
||||
isDefault: isPropertyAtDefault({
|
||||
hasAnimatedKeyframes: scaleY.hasAnimatedKeyframes,
|
||||
isPlayheadWithinElementRange,
|
||||
resolvedValue: resolvedTransform.scaleY,
|
||||
staticValue: element.transform.scaleY,
|
||||
defaultValue: DEFAULTS.element.transform.scaleY,
|
||||
}),
|
||||
};
|
||||
|
||||
const rotation = useKeyframedNumberProperty({
|
||||
trackId,
|
||||
elementId: element.id,
|
||||
animations: element.animations,
|
||||
propertyPath: "transform.rotate",
|
||||
localTime,
|
||||
isPlayheadWithinElementRange,
|
||||
displayValue: Math.round(resolvedTransform.rotate).toString(),
|
||||
parse: (input) => {
|
||||
const parsed = parseNumericInput({ input });
|
||||
if (parsed === null) return null;
|
||||
return clamp({ value: parsed, min: -360, max: 360 });
|
||||
},
|
||||
valueAtPlayhead: resolvedTransform.rotate,
|
||||
step: 1,
|
||||
buildBaseUpdates: ({ value }) => ({
|
||||
transform: {
|
||||
...element.transform,
|
||||
rotate: value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const hasScaleKeyframe = hasGroupKeyframeAtTime({
|
||||
animations: element.animations,
|
||||
group: "transform.scale",
|
||||
time: localTime,
|
||||
});
|
||||
|
||||
const toggleScaleKeyframe = () => {
|
||||
if (!isPlayheadWithinElementRange) return;
|
||||
const existing = getGroupKeyframesAtTime({
|
||||
animations: element.animations,
|
||||
group: "transform.scale",
|
||||
time: localTime,
|
||||
});
|
||||
if (existing.length > 0) {
|
||||
editor.timeline.removeKeyframes({
|
||||
keyframes: existing.map((ref) => ({
|
||||
trackId,
|
||||
elementId: element.id,
|
||||
...ref,
|
||||
})),
|
||||
});
|
||||
return;
|
||||
}
|
||||
editor.timeline.upsertKeyframes({
|
||||
keyframes: [
|
||||
{
|
||||
trackId,
|
||||
elementId: element.id,
|
||||
propertyPath: "transform.scaleX",
|
||||
time: localTime,
|
||||
value: resolvedTransform.scaleX,
|
||||
},
|
||||
{
|
||||
trackId,
|
||||
elementId: element.id,
|
||||
propertyPath: "transform.scaleY",
|
||||
time: localTime,
|
||||
value: resolvedTransform.scaleY,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const scaleLockButton = (
|
||||
<Button
|
||||
type="button"
|
||||
variant={isScaleLocked ? "secondary" : "ghost"}
|
||||
size="icon"
|
||||
aria-pressed={isScaleLocked}
|
||||
onClick={() => setTransformScaleLocked(!isScaleLocked)}
|
||||
>
|
||||
<HugeiconsIcon icon={Link05Icon} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Section collapsible sectionKey={`${element.id}:transform`}>
|
||||
<SectionHeader>
|
||||
<SectionTitle>Transform</SectionTitle>
|
||||
</SectionHeader>
|
||||
<SectionContent>
|
||||
<SectionFields>
|
||||
<div className="flex items-end gap-2">
|
||||
{isScaleLocked ? (
|
||||
<>
|
||||
<SectionField
|
||||
label="Scale"
|
||||
className="min-w-0 flex-1"
|
||||
beforeLabel={
|
||||
<KeyframeToggle
|
||||
isActive={hasScaleKeyframe}
|
||||
isDisabled={!isPlayheadWithinElementRange}
|
||||
title="Toggle scale keyframe"
|
||||
onToggle={toggleScaleKeyframe}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NumberField
|
||||
icon={<HugeiconsIcon icon={ArrowExpandIcon} />}
|
||||
{...scaleFieldPropsX}
|
||||
/>
|
||||
</SectionField>
|
||||
{scaleLockButton}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SectionField
|
||||
label="Width"
|
||||
className="min-w-0 flex-1"
|
||||
beforeLabel={
|
||||
<KeyframeToggle
|
||||
isActive={scaleX.isKeyframedAtTime}
|
||||
isDisabled={!isPlayheadWithinElementRange}
|
||||
title="Toggle width scale keyframe"
|
||||
onToggle={scaleX.toggleKeyframe}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NumberField icon="W" {...scaleFieldPropsX} />
|
||||
</SectionField>
|
||||
{scaleLockButton}
|
||||
<SectionField
|
||||
label="Height"
|
||||
className="min-w-0 flex-1"
|
||||
beforeLabel={
|
||||
<KeyframeToggle
|
||||
isActive={scaleY.isKeyframedAtTime}
|
||||
isDisabled={!isPlayheadWithinElementRange}
|
||||
title="Toggle height scale keyframe"
|
||||
onToggle={scaleY.toggleKeyframe}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NumberField icon="H" {...scaleFieldPropsY} />
|
||||
</SectionField>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<SectionField
|
||||
label="X"
|
||||
className="min-w-0 flex-1"
|
||||
beforeLabel={
|
||||
<KeyframeToggle
|
||||
isActive={positionX.isKeyframedAtTime}
|
||||
isDisabled={!isPlayheadWithinElementRange}
|
||||
title="Toggle X position keyframe"
|
||||
onToggle={positionX.toggleKeyframe}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NumberField
|
||||
icon="X"
|
||||
value={positionX.displayValue}
|
||||
onFocus={positionX.onFocus}
|
||||
onChange={positionX.onChange}
|
||||
onBlur={positionX.onBlur}
|
||||
onScrub={positionX.scrubTo}
|
||||
onScrubEnd={positionX.commitScrub}
|
||||
onReset={() =>
|
||||
positionX.commitValue({
|
||||
value: DEFAULTS.element.transform.position.x,
|
||||
})
|
||||
}
|
||||
isDefault={isPropertyAtDefault({
|
||||
hasAnimatedKeyframes: positionX.hasAnimatedKeyframes,
|
||||
isPlayheadWithinElementRange,
|
||||
resolvedValue: resolvedTransform.position.x,
|
||||
staticValue: element.transform.position.x,
|
||||
defaultValue: DEFAULTS.element.transform.position.x,
|
||||
})}
|
||||
/>
|
||||
</SectionField>
|
||||
<SectionField
|
||||
label="Y"
|
||||
className="min-w-0 flex-1"
|
||||
beforeLabel={
|
||||
<KeyframeToggle
|
||||
isActive={positionY.isKeyframedAtTime}
|
||||
isDisabled={!isPlayheadWithinElementRange}
|
||||
title="Toggle Y position keyframe"
|
||||
onToggle={positionY.toggleKeyframe}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NumberField
|
||||
icon="Y"
|
||||
value={positionY.displayValue}
|
||||
onFocus={positionY.onFocus}
|
||||
onChange={positionY.onChange}
|
||||
onBlur={positionY.onBlur}
|
||||
onScrub={positionY.scrubTo}
|
||||
onScrubEnd={positionY.commitScrub}
|
||||
onReset={() =>
|
||||
positionY.commitValue({
|
||||
value: DEFAULTS.element.transform.position.y,
|
||||
})
|
||||
}
|
||||
isDefault={isPropertyAtDefault({
|
||||
hasAnimatedKeyframes: positionY.hasAnimatedKeyframes,
|
||||
isPlayheadWithinElementRange,
|
||||
resolvedValue: resolvedTransform.position.y,
|
||||
staticValue: element.transform.position.y,
|
||||
defaultValue: DEFAULTS.element.transform.position.y,
|
||||
})}
|
||||
/>
|
||||
</SectionField>
|
||||
</div>
|
||||
|
||||
<SectionField
|
||||
label="Rotation"
|
||||
beforeLabel={
|
||||
<KeyframeToggle
|
||||
isActive={rotation.isKeyframedAtTime}
|
||||
isDisabled={!isPlayheadWithinElementRange}
|
||||
title="Toggle rotation keyframe"
|
||||
onToggle={rotation.toggleKeyframe}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<NumberField
|
||||
icon={<HugeiconsIcon icon={RotateClockwiseIcon} />}
|
||||
className="flex-none"
|
||||
value={rotation.displayValue}
|
||||
onFocus={rotation.onFocus}
|
||||
onChange={rotation.onChange}
|
||||
onBlur={rotation.onBlur}
|
||||
dragSensitivity="slow"
|
||||
onScrub={rotation.scrubTo}
|
||||
onScrubEnd={rotation.commitScrub}
|
||||
onReset={() =>
|
||||
rotation.commitValue({
|
||||
value: DEFAULTS.element.transform.rotate,
|
||||
})
|
||||
}
|
||||
isDefault={isPropertyAtDefault({
|
||||
hasAnimatedKeyframes: rotation.hasAnimatedKeyframes,
|
||||
isPlayheadWithinElementRange,
|
||||
resolvedValue: resolvedTransform.rotate,
|
||||
staticValue: element.transform.rotate,
|
||||
defaultValue: DEFAULTS.element.transform.rotate,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</SectionField>
|
||||
</SectionFields>
|
||||
</SectionContent>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user