Skip to content

Commit

Permalink
Animate
Browse files Browse the repository at this point in the history
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
istarkov committed Feb 18, 2025
1 parent cfbddfe commit 01dc296
Show file tree
Hide file tree
Showing 48 changed files with 2,269 additions and 297 deletions.
8 changes: 7 additions & 1 deletion apps/builder/app/builder/features/components/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,15 @@ const $metas = computed(
const availableComponents = new Set<string>();
const metas: Meta[] = [];
for (const [name, componentMeta] of componentMetas) {
if (
isFeatureEnabled("animation") === false &&
name.endsWith(":AnimateChildren")
) {
continue;
}

// only set available components from component meta
availableComponents.add(name);

if (
isFeatureEnabled("headSlotComponent") === false &&
name === "HeadSlot"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ export const renderControl = ({
);
}

if (prop.type === "animationAction") {
throw new Error(
`Cannot render a fallback control for prop "${rest.propName}" with type animationAction`
);
}

prop satisfies never;
}

Expand Down
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>
);
};
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: () => {},
},
};
Loading

0 comments on commit 01dc296

Please sign in to comment.