Skip to content

Commit 14da057

Browse files
committed
Animate
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 Fix hook Allow calc Add ability to play localy isPinned Add mutations to track Support composition Switch auto on add Add animation-composition Use default composite
1 parent 18409bb commit 14da057

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2352
-300
lines changed

apps/builder/app/builder/features/components/components.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,15 @@ const $metas = computed(
5959
const availableComponents = new Set<string>();
6060
const metas: Meta[] = [];
6161
for (const [name, componentMeta] of componentMetas) {
62+
if (
63+
isFeatureEnabled("animation") === false &&
64+
name.endsWith(":AnimateChildren")
65+
) {
66+
continue;
67+
}
68+
6269
// only set available components from component meta
6370
availableComponents.add(name);
64-
6571
if (
6672
isFeatureEnabled("headSlotComponent") === false &&
6773
name === "HeadSlot"

apps/builder/app/builder/features/settings-panel/controls/combined.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ export const renderControl = ({
216216
);
217217
}
218218

219+
if (prop.type === "animationAction") {
220+
throw new Error(
221+
`Cannot render a fallback control for prop "${rest.propName}" with type animationAction`
222+
);
223+
}
224+
219225
prop satisfies never;
220226
}
221227

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { parseCss } from "@webstudio-is/css-data";
2+
import { StyleValue, toValue } from "@webstudio-is/css-engine";
3+
import {
4+
Text,
5+
Grid,
6+
IconButton,
7+
Label,
8+
Separator,
9+
Tooltip,
10+
} from "@webstudio-is/design-system";
11+
import { MinusIcon, PlusIcon } from "@webstudio-is/icons";
12+
import type { AnimationKeyframe } from "@webstudio-is/sdk";
13+
import { Fragment, useMemo, useState } from "react";
14+
import {
15+
CssValueInput,
16+
type IntermediateStyleValue,
17+
} from "~/builder/features/style-panel/shared/css-value-input";
18+
import { CodeEditor } from "~/builder/shared/code-editor";
19+
import { useIds } from "~/shared/form-utils";
20+
21+
const unitOptions = [
22+
{
23+
id: "%" as const,
24+
label: "%",
25+
type: "unit" as const,
26+
},
27+
];
28+
29+
const OffsetInput = ({
30+
id,
31+
value,
32+
onChange,
33+
}: {
34+
id: string;
35+
value: number | undefined;
36+
onChange: (value: number | undefined) => void;
37+
}) => {
38+
const [intermediateValue, setIntermediateValue] = useState<
39+
StyleValue | IntermediateStyleValue
40+
>();
41+
42+
return (
43+
<CssValueInput
44+
id={id}
45+
placeholder="auto"
46+
getOptions={() => []}
47+
unitOptions={unitOptions}
48+
intermediateValue={intermediateValue}
49+
styleSource="default"
50+
/* same as offset has 0 - 100% */
51+
property={"fontStretch"}
52+
value={
53+
value !== undefined
54+
? {
55+
type: "unit",
56+
value: Math.round(value * 1000) / 10,
57+
unit: "%",
58+
}
59+
: undefined
60+
}
61+
onChange={(styleValue) => {
62+
if (styleValue === undefined) {
63+
setIntermediateValue(styleValue);
64+
return;
65+
}
66+
67+
const clampedStyleValue = { ...styleValue };
68+
if (
69+
clampedStyleValue.type === "unit" &&
70+
clampedStyleValue.unit === "%"
71+
) {
72+
clampedStyleValue.value = Math.min(
73+
100,
74+
Math.max(0, clampedStyleValue.value)
75+
);
76+
}
77+
78+
setIntermediateValue(clampedStyleValue);
79+
}}
80+
onHighlight={(_styleValue) => {
81+
/* @todo: think about preview */
82+
}}
83+
onChangeComplete={(event) => {
84+
setIntermediateValue(undefined);
85+
86+
if (event.value.type === "unit" && event.value.unit === "%") {
87+
onChange(Math.min(100, Math.max(0, event.value.value)) / 100);
88+
return;
89+
}
90+
91+
setIntermediateValue({
92+
type: "invalid",
93+
value: toValue(event.value),
94+
});
95+
}}
96+
onAbort={() => {
97+
/* @todo: allow to change some ephemeral property to see the result in action */
98+
}}
99+
onReset={() => {
100+
setIntermediateValue(undefined);
101+
onChange(undefined);
102+
}}
103+
/>
104+
);
105+
};
106+
107+
const Keyframe = ({
108+
value,
109+
onChange,
110+
}: {
111+
value: AnimationKeyframe;
112+
onChange: (value: AnimationKeyframe | undefined) => void;
113+
}) => {
114+
const ids = useIds(["offset"]);
115+
116+
const cssProperties = useMemo(() => {
117+
let result = ``;
118+
for (const [property, style] of Object.entries(value.styles)) {
119+
result = `${result}${property}: ${toValue(style)};\n`;
120+
}
121+
return result;
122+
}, [value.styles]);
123+
124+
return (
125+
<>
126+
<Grid
127+
gap={1}
128+
align={"center"}
129+
css={{ gridTemplateColumns: "1fr 1fr auto" }}
130+
>
131+
<Label htmlFor={ids.offset}>Offset</Label>
132+
<OffsetInput
133+
id={ids.offset}
134+
value={value.offset}
135+
onChange={(offset) => {
136+
onChange({ ...value, offset });
137+
}}
138+
/>
139+
<Tooltip content="Remove keyframe">
140+
<IconButton onClick={() => onChange(undefined)}>
141+
<MinusIcon />
142+
</IconButton>
143+
</Tooltip>
144+
</Grid>
145+
<Grid>
146+
<CodeEditor
147+
lang="css-properties"
148+
size="keyframe"
149+
value={cssProperties}
150+
onChange={() => {
151+
/* do nothing */
152+
}}
153+
onChangeComplete={(cssText) => {
154+
const parsedStyles = parseCss(`selector{${cssText}}`);
155+
onChange({
156+
...value,
157+
styles: parsedStyles.reduce(
158+
(r, { property, value }) => ({ ...r, [property]: value }),
159+
{}
160+
),
161+
});
162+
}}
163+
/>
164+
</Grid>
165+
</>
166+
);
167+
};
168+
169+
export const Keyframes = ({
170+
value: keyframes,
171+
onChange,
172+
}: {
173+
value: AnimationKeyframe[];
174+
onChange: (value: AnimationKeyframe[]) => void;
175+
}) => {
176+
const ids = useIds(["addKeyframe"]);
177+
178+
return (
179+
<Grid gap={2}>
180+
<Grid gap={1} align={"center"} css={{ gridTemplateColumns: "1fr auto" }}>
181+
<Label htmlFor={ids.addKeyframe}>
182+
<Text variant={"titles"}>Keyframes</Text>
183+
</Label>
184+
<IconButton
185+
id={ids.addKeyframe}
186+
onClick={() =>
187+
onChange([...keyframes, { offset: undefined, styles: {} }])
188+
}
189+
>
190+
<PlusIcon />
191+
</IconButton>
192+
</Grid>
193+
194+
{keyframes.map((value, index) => (
195+
<Fragment key={index}>
196+
<Separator />
197+
<Keyframe
198+
key={index}
199+
value={value}
200+
onChange={(newValue) => {
201+
if (newValue === undefined) {
202+
const newValues = [...keyframes];
203+
newValues.splice(index, 1);
204+
onChange(newValues);
205+
return;
206+
}
207+
208+
const newValues = [...keyframes];
209+
newValues[index] = newValue;
210+
onChange(newValues);
211+
}}
212+
/>
213+
</Fragment>
214+
))}
215+
</Grid>
216+
);
217+
};
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { AnimationPanelContent } from "./animation-panel-content";
3+
import { theme } from "@webstudio-is/design-system";
4+
import { useState } from "react";
5+
import type { ScrollAnimation, ViewAnimation } from "@webstudio-is/sdk";
6+
7+
const meta = {
8+
title: "Builder/Settings Panel/Animation Panel Content",
9+
component: AnimationPanelContent,
10+
parameters: {
11+
layout: "centered",
12+
},
13+
decorators: [
14+
(Story) => (
15+
<div style={{ background: theme.colors.backgroundPanel, padding: 16 }}>
16+
<Story />
17+
</div>
18+
),
19+
],
20+
} satisfies Meta<typeof AnimationPanelContent>;
21+
22+
export default meta;
23+
type Story = StoryObj<typeof meta>;
24+
25+
const ScrollAnimationTemplate: Story["render"] = ({ value: initialValue }) => {
26+
const [value, setValue] = useState(initialValue);
27+
28+
return (
29+
<AnimationPanelContent
30+
type="scroll"
31+
value={value}
32+
onChange={(newValue) => {
33+
setValue(newValue as ScrollAnimation);
34+
}}
35+
/>
36+
);
37+
};
38+
39+
const ViewAnimationTemplate: Story["render"] = ({ value: initialValue }) => {
40+
const [value, setValue] = useState(initialValue);
41+
42+
return (
43+
<AnimationPanelContent
44+
type="view"
45+
value={value}
46+
onChange={(newValue) => {
47+
setValue(newValue as ViewAnimation);
48+
}}
49+
/>
50+
);
51+
};
52+
53+
export const ScrollAnimationStory: Story = {
54+
render: ScrollAnimationTemplate,
55+
args: {
56+
type: "scroll",
57+
value: {
58+
name: "scroll-animation",
59+
timing: {
60+
rangeStart: ["start", { type: "unit", value: 0, unit: "%" }],
61+
rangeEnd: ["end", { type: "unit", value: 100, unit: "%" }],
62+
},
63+
keyframes: [
64+
{
65+
offset: 0,
66+
styles: {
67+
opacity: { type: "unit", value: 0, unit: "%" },
68+
color: { type: "rgb", r: 255, g: 0, b: 0, alpha: 1 },
69+
},
70+
},
71+
],
72+
},
73+
onChange: () => {},
74+
},
75+
};
76+
77+
export const ViewAnimationStory: Story = {
78+
render: ViewAnimationTemplate,
79+
args: {
80+
type: "view",
81+
value: {
82+
name: "view-animation",
83+
timing: {
84+
rangeStart: ["entry", { type: "unit", value: 0, unit: "%" }],
85+
rangeEnd: ["exit", { type: "unit", value: 100, unit: "%" }],
86+
},
87+
keyframes: [
88+
{
89+
offset: 0,
90+
styles: {
91+
opacity: { type: "unit", value: 0, unit: "%" },
92+
color: { type: "rgb", r: 255, g: 0, b: 0, alpha: 1 },
93+
},
94+
},
95+
],
96+
},
97+
onChange: () => {},
98+
},
99+
};

0 commit comments

Comments
 (0)