Skip to content

Commit 6f1a94c

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 Fix tests Allow select self Add kebab Change private detection refactor: output hyphenated properties from css parser (#4900) We are going to switch to hyphenated properties in styles. Here refactored css parser to output hyphenated property instead of camel case and added camelCaseProperty utility which does the opposite of hyphenateProperty. fix my env
1 parent f5b7efe commit 6f1a94c

Some content is hidden

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

58 files changed

+2410
-319
lines changed

.devcontainer/postinstall.sh

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,14 @@ sudo rm -rf /tmp/corepack-cache
1414
sudo rm -rf /usr/local/lib/node_modules/corepack # Manually remove global corepack
1515

1616
# Reinstall corepack globally via npm
17-
npm install -g corepack@latest # Install latest corepack version
17+
npm install -g corepack@latest --force # Install latest corepack version
1818
sudo corepack enable # Re-enable corepack
1919

2020
# Check corepack version after reinstall
21-
echo "--- Corepack version after reinstall ---"
2221
corepack --version
23-
echo "--- End corepack version check ---"
24-
2522

2623
# Prepare pnpm (again, after corepack reinstall)
27-
sudo corepack prepare [email protected] --activate
24+
corepack prepare [email protected] --activate
2825

2926
# Go to workspace directory
3027
cd /workspaces/webstudio
@@ -42,7 +39,7 @@ find . -name '.pnpm-store' -type d -prune -exec rm -rf '{}' +
4239

4340
# Install dependencies, build, and migrate
4441
pnpm install
45-
pnpm run build
42+
pnpm build
4643
pnpm migrations migrate
4744

4845
# Add git aliases

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/pages/page-settings.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1631,7 +1631,16 @@ export const PageSettings = ({
16311631
onDuplicate={() => {
16321632
const newPageId = duplicatePage(pageId);
16331633
if (newPageId !== undefined) {
1634-
onDuplicate(newPageId);
1634+
// In `canvas.tsx`, within `subscribeStyles`, we use `requestAnimationFrame` (RAF) for style recalculation.
1635+
// After `duplicatePage`, styles are not yet recalculated.
1636+
// To ensure they are properly updated, we use double RAF.
1637+
requestAnimationFrame(() => {
1638+
// At this tick styles are updating
1639+
requestAnimationFrame(() => {
1640+
// At this tick styles are updated
1641+
onDuplicate(newPageId);
1642+
});
1643+
});
16351644
}
16361645
}}
16371646
>

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