-
Notifications
You must be signed in to change notification settings - Fork 857
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Animate 2 Animation UI - Type Select Subject selection Create new Animation New animation Add Add sort/delete subject Add Ranges Select Fix scroll Ranges Ready Fix comments Allow negative values Add storybook Add storie Add error Add offset Add anim Edit keyframes Add Fill Mode Add easing Rename Fix errors Hide animate children Add hook upd Fix
- Loading branch information
Showing
48 changed files
with
2,269 additions
and
297 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
217 changes: 217 additions & 0 deletions
217
...ilder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import { parseCss } from "@webstudio-is/css-data"; | ||
import { StyleValue, toValue } from "@webstudio-is/css-engine"; | ||
import { | ||
Text, | ||
Grid, | ||
IconButton, | ||
Label, | ||
Separator, | ||
Tooltip, | ||
} from "@webstudio-is/design-system"; | ||
import { MinusIcon, PlusIcon } from "@webstudio-is/icons"; | ||
import type { AnimationKeyframe } from "@webstudio-is/sdk"; | ||
import { Fragment, useMemo, useState } from "react"; | ||
import { | ||
CssValueInput, | ||
type IntermediateStyleValue, | ||
} from "~/builder/features/style-panel/shared/css-value-input"; | ||
import { CodeEditor } from "~/builder/shared/code-editor"; | ||
import { useIds } from "~/shared/form-utils"; | ||
|
||
const unitOptions = [ | ||
{ | ||
id: "%" as const, | ||
label: "%", | ||
type: "unit" as const, | ||
}, | ||
]; | ||
|
||
const OffsetInput = ({ | ||
id, | ||
value, | ||
onChange, | ||
}: { | ||
id: string; | ||
value: number | undefined; | ||
onChange: (value: number | undefined) => void; | ||
}) => { | ||
const [intermediateValue, setIntermediateValue] = useState< | ||
StyleValue | IntermediateStyleValue | ||
>(); | ||
|
||
return ( | ||
<CssValueInput | ||
id={id} | ||
placeholder="auto" | ||
getOptions={() => []} | ||
unitOptions={unitOptions} | ||
intermediateValue={intermediateValue} | ||
styleSource="default" | ||
/* same as offset has 0 - 100% */ | ||
property={"fontStretch"} | ||
value={ | ||
value !== undefined | ||
? { | ||
type: "unit", | ||
value: Math.round(value * 1000) / 10, | ||
unit: "%", | ||
} | ||
: undefined | ||
} | ||
onChange={(styleValue) => { | ||
if (styleValue === undefined) { | ||
setIntermediateValue(styleValue); | ||
return; | ||
} | ||
|
||
const clampedStyleValue = { ...styleValue }; | ||
if ( | ||
clampedStyleValue.type === "unit" && | ||
clampedStyleValue.unit === "%" | ||
) { | ||
clampedStyleValue.value = Math.min( | ||
100, | ||
Math.max(0, clampedStyleValue.value) | ||
); | ||
} | ||
|
||
setIntermediateValue(clampedStyleValue); | ||
}} | ||
onHighlight={(_styleValue) => { | ||
/* @todo: think about preview */ | ||
}} | ||
onChangeComplete={(event) => { | ||
setIntermediateValue(undefined); | ||
|
||
if (event.value.type === "unit" && event.value.unit === "%") { | ||
onChange(Math.min(100, Math.max(0, event.value.value)) / 100); | ||
return; | ||
} | ||
|
||
setIntermediateValue({ | ||
type: "invalid", | ||
value: toValue(event.value), | ||
}); | ||
}} | ||
onAbort={() => { | ||
/* @todo: allow to change some ephemeral property to see the result in action */ | ||
}} | ||
onReset={() => { | ||
setIntermediateValue(undefined); | ||
onChange(undefined); | ||
}} | ||
/> | ||
); | ||
}; | ||
|
||
const Keyframe = ({ | ||
value, | ||
onChange, | ||
}: { | ||
value: AnimationKeyframe; | ||
onChange: (value: AnimationKeyframe | undefined) => void; | ||
}) => { | ||
const ids = useIds(["offset"]); | ||
|
||
const cssProperties = useMemo(() => { | ||
let result = ``; | ||
for (const [property, style] of Object.entries(value.styles)) { | ||
result = `${result}${property}: ${toValue(style)};\n`; | ||
} | ||
return result; | ||
}, [value.styles]); | ||
|
||
return ( | ||
<> | ||
<Grid | ||
gap={1} | ||
align={"center"} | ||
css={{ gridTemplateColumns: "1fr 1fr auto" }} | ||
> | ||
<Label htmlFor={ids.offset}>Offset</Label> | ||
<OffsetInput | ||
id={ids.offset} | ||
value={value.offset} | ||
onChange={(offset) => { | ||
onChange({ ...value, offset }); | ||
}} | ||
/> | ||
<Tooltip content="Remove keyframe"> | ||
<IconButton onClick={() => onChange(undefined)}> | ||
<MinusIcon /> | ||
</IconButton> | ||
</Tooltip> | ||
</Grid> | ||
<Grid> | ||
<CodeEditor | ||
lang="css-properties" | ||
size="keyframe" | ||
value={cssProperties} | ||
onChange={() => { | ||
/* do nothing */ | ||
}} | ||
onChangeComplete={(cssText) => { | ||
const parsedStyles = parseCss(`selector{${cssText}}`); | ||
onChange({ | ||
...value, | ||
styles: parsedStyles.reduce( | ||
(r, { property, value }) => ({ ...r, [property]: value }), | ||
{} | ||
), | ||
}); | ||
}} | ||
/> | ||
</Grid> | ||
</> | ||
); | ||
}; | ||
|
||
export const Keyframes = ({ | ||
value: keyframes, | ||
onChange, | ||
}: { | ||
value: AnimationKeyframe[]; | ||
onChange: (value: AnimationKeyframe[]) => void; | ||
}) => { | ||
const ids = useIds(["addKeyframe"]); | ||
|
||
return ( | ||
<Grid gap={2}> | ||
<Grid gap={1} align={"center"} css={{ gridTemplateColumns: "1fr auto" }}> | ||
<Label htmlFor={ids.addKeyframe}> | ||
<Text variant={"titles"}>Keyframes</Text> | ||
</Label> | ||
<IconButton | ||
id={ids.addKeyframe} | ||
onClick={() => | ||
onChange([...keyframes, { offset: undefined, styles: {} }]) | ||
} | ||
> | ||
<PlusIcon /> | ||
</IconButton> | ||
</Grid> | ||
|
||
{keyframes.map((value, index) => ( | ||
<Fragment key={index}> | ||
<Separator /> | ||
<Keyframe | ||
key={index} | ||
value={value} | ||
onChange={(newValue) => { | ||
if (newValue === undefined) { | ||
const newValues = [...keyframes]; | ||
newValues.splice(index, 1); | ||
onChange(newValues); | ||
return; | ||
} | ||
|
||
const newValues = [...keyframes]; | ||
newValues[index] = newValue; | ||
onChange(newValues); | ||
}} | ||
/> | ||
</Fragment> | ||
))} | ||
</Grid> | ||
); | ||
}; |
99 changes: 99 additions & 0 deletions
99
...ilder/features/settings-panel/props-section/animation/animation-panel-content.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
import { AnimationPanelContent } from "./animation-panel-content"; | ||
import { theme } from "@webstudio-is/design-system"; | ||
import { useState } from "react"; | ||
import type { ScrollAnimation, ViewAnimation } from "@webstudio-is/sdk"; | ||
|
||
const meta = { | ||
title: "Builder/Settings Panel/Animation Panel Content", | ||
component: AnimationPanelContent, | ||
parameters: { | ||
layout: "centered", | ||
}, | ||
decorators: [ | ||
(Story) => ( | ||
<div style={{ background: theme.colors.backgroundPanel, padding: 16 }}> | ||
<Story /> | ||
</div> | ||
), | ||
], | ||
} satisfies Meta<typeof AnimationPanelContent>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
const ScrollAnimationTemplate: Story["render"] = ({ value: initialValue }) => { | ||
const [value, setValue] = useState(initialValue); | ||
|
||
return ( | ||
<AnimationPanelContent | ||
type="scroll" | ||
value={value} | ||
onChange={(newValue) => { | ||
setValue(newValue as ScrollAnimation); | ||
}} | ||
/> | ||
); | ||
}; | ||
|
||
const ViewAnimationTemplate: Story["render"] = ({ value: initialValue }) => { | ||
const [value, setValue] = useState(initialValue); | ||
|
||
return ( | ||
<AnimationPanelContent | ||
type="view" | ||
value={value} | ||
onChange={(newValue) => { | ||
setValue(newValue as ViewAnimation); | ||
}} | ||
/> | ||
); | ||
}; | ||
|
||
export const ScrollAnimationStory: Story = { | ||
render: ScrollAnimationTemplate, | ||
args: { | ||
type: "scroll", | ||
value: { | ||
name: "scroll-animation", | ||
timing: { | ||
rangeStart: ["start", { type: "unit", value: 0, unit: "%" }], | ||
rangeEnd: ["end", { type: "unit", value: 100, unit: "%" }], | ||
}, | ||
keyframes: [ | ||
{ | ||
offset: 0, | ||
styles: { | ||
opacity: { type: "unit", value: 0, unit: "%" }, | ||
color: { type: "rgb", r: 255, g: 0, b: 0, alpha: 1 }, | ||
}, | ||
}, | ||
], | ||
}, | ||
onChange: () => {}, | ||
}, | ||
}; | ||
|
||
export const ViewAnimationStory: Story = { | ||
render: ViewAnimationTemplate, | ||
args: { | ||
type: "view", | ||
value: { | ||
name: "view-animation", | ||
timing: { | ||
rangeStart: ["entry", { type: "unit", value: 0, unit: "%" }], | ||
rangeEnd: ["exit", { type: "unit", value: 100, unit: "%" }], | ||
}, | ||
keyframes: [ | ||
{ | ||
offset: 0, | ||
styles: { | ||
opacity: { type: "unit", value: 0, unit: "%" }, | ||
color: { type: "rgb", r: 255, g: 0, b: 0, alpha: 1 }, | ||
}, | ||
}, | ||
], | ||
}, | ||
onChange: () => {}, | ||
}, | ||
}; |
Oops, something went wrong.