experimental: add ui controls for managing perspective-origin from transforms (#3950)

## Description

This PR helps in managing perspective origin which is similar to
transform origin under transform section.

## Steps for reproduction

- Click perspective-origin from the dropdown on transform section.
- Can change the values of x and y axises using the positional grid.
- Can change values of x and y using the individual inputs by
scrub/manually entering/keyboard up&down
- Add values for perspective -origin from the advanced tab and update
those values using the UI controls.
- Reset the property using the label indicator.
Use keywords from the suggestions dropdown for x and y axis like left,
top, center etc.

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [x] made a self-review
- [x] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [x] tested locally and on preview environment (preview dev login:
5de6)
- [x] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file



![perspective-origin](https://github.com/user-attachments/assets/ec9af84d-2e3a-44f9-add7-41d58661cbd5)
This commit is contained in:
Jaya Krishna Namburu authored and GitHub committed 2024-08-19 09:37:17 +02:00
1 parent b5585f850d
commit cb73ab65bb
4 files changed
+104 -45

No files matched your search

@@ -19,8 +19,7 @@ import { useMemo } from "react";
import { extractTransformOrPerspectiveOriginValues } from "./transform-extractors";
import { CssValueInputContainer } from "../../shared/css-value-input";
import type { StyleUpdateOptions } from "../../shared/use-style-data";
const property: StyleProperty = "transformOrigin";
import { calculatePositionFromOrigin } from "./transform-utils";
// Fake properties to use in the CssValueInputContainer
// x, y axis takes length | percentage | keyword
@@ -29,44 +28,22 @@ const fakePropertyX: StyleProperty = "backgroundPositionX";
const fakePropertyY: StyleProperty = "backgroundPositionY";
const fakePropertyZ: StyleProperty = "outlineOffset";
const keyworkToValue: Record<string, number> = {
left: 0,
right: 100,
center: 50,
top: 0,
bottom: 100,
};
const calculateBackgroundPosition = (value: StyleValue | undefined) => {
if (value === undefined) {
return 50;
}
if (value.type === "unit") {
return value.value;
}
if (value.type === "keyword") {
return keyworkToValue[value.value];
}
return 0;
};
export const TransformOrigin = (props: SectionProps) => {
const { currentStyle, deleteProperty, setProperty } = props;
export const TransformAndPerspectiveOrigin = (
props: SectionProps & { property: StyleProperty }
) => {
const { currentStyle, deleteProperty, setProperty, property } = props;
const value = currentStyle[property]?.local;
const { label } = styleConfigByName(property);
const origin = useMemo(() => {
if (value?.type !== "tuple") {
if (value?.type !== "tuple" && value?.type !== "keyword") {
return;
}
return extractTransformOrPerspectiveOriginValues(value);
}, [value]);
const xInfo = useMemo(() => calculateBackgroundPosition(origin?.x), [origin]);
const yInfo = useMemo(() => calculateBackgroundPosition(origin?.y), [origin]);
const xInfo = useMemo(() => calculatePositionFromOrigin(origin?.x), [origin]);
const yInfo = useMemo(() => calculatePositionFromOrigin(origin?.y), [origin]);
const xOriginKeywords: Array<KeywordValue> = ["left", "center", "right"].map(
(value) => ({
type: "keyword",
@@ -80,7 +57,7 @@ export const TransformOrigin = (props: SectionProps) => {
})
);
if (value?.type !== "tuple" || origin === undefined) {
if (origin === undefined) {
return;
}
@@ -140,7 +117,7 @@ export const TransformOrigin = (props: SectionProps) => {
],
};
if (origin.z !== undefined) {
if (property === "transformOrigin" && origin.z !== undefined) {
value.value.push(origin.z);
}
@@ -194,7 +171,7 @@ export const TransformOrigin = (props: SectionProps) => {
}
/>
</Flex>
{origin.z !== undefined && (
{property === "transformOrigin" && origin.z !== undefined && (
<Flex gap="2" align="center">
<Label>Z</Label>
<CssValueInputContainer
@@ -64,16 +64,46 @@ const isValidTransformOriginValue = (
// Both transform and perspective origin shares the same syntax for their values.
// The only difference is `transform-origin` can have a 3rd value for z-axis.
export const extractTransformOrPerspectiveOriginValues = (
value: TupleValue
value: TupleValue | KeywordValue
): {
x: KeywordValue | UnitValue;
y: KeywordValue | UnitValue;
z?: UnitValue;
} => {
const xAxisKeywordValues: Array<string> = ["left", "right"];
const yAxisKeywordValues: Array<string> = ["top", "bottom"];
let x: KeywordValue | UnitValue = { type: "keyword", value: "center" };
let y: KeywordValue | UnitValue = { type: "keyword", value: "center" };
let z: UnitValue | undefined;
if (value.type === "keyword") {
if (value.value === "center") {
return {
x: value,
y: value,
};
}
if (yAxisKeywordValues.includes(value.value)) {
return {
x: x,
y: value,
};
}
if (xAxisKeywordValues.includes(value.value)) {
return {
x: value,
y: y,
};
}
return {
x: value,
y: value,
};
}
// https://www.w3.org/TR/css-transforms-1/#transform-origin-property
// If only one value is specified, the second value is assumed to be center.
if (value.value.length === 1 && value.value[0].type === "unit") {
@@ -81,11 +111,11 @@ export const extractTransformOrPerspectiveOriginValues = (
}
if (value.value.length === 1 && value.value[0].type === "keyword") {
if (["top", "bottom"].includes(value.value[0].value)) {
if (yAxisKeywordValues.includes(value.value[0].value)) {
y = value.value[0];
}
if (["left", "right"].includes(value.value[0].value)) {
if (xAxisKeywordValues.includes(value.value[0].value)) {
x = value.value[0];
}
}
@@ -103,12 +133,15 @@ export const extractTransformOrPerspectiveOriginValues = (
x = first;
y = second;
if (first.type === "keyword" && ["left", "right"].includes(first.value)) {
if (
first.type === "keyword" &&
xAxisKeywordValues.includes(first.value)
) {
x = first;
y = second;
} else if (
first.type === "keyword" &&
["top", "bottom"].includes(first.value)
yAxisKeywordValues.includes(first.value)
) {
y = first;
x = second;
@@ -127,12 +160,15 @@ export const extractTransformOrPerspectiveOriginValues = (
y = second;
z = third;
if (first.type === "keyword" && ["left", "right"].includes(first.value)) {
if (
first.type === "keyword" &&
xAxisKeywordValues.includes(first.value)
) {
x = first;
y = second;
} else if (
first.type === "keyword" &&
["top", "bottom"].includes(first.value)
yAxisKeywordValues.includes(first.value)
) {
y = first;
x = second;
@@ -1,6 +1,7 @@
import { parseCssValue } from "@webstudio-is/css-data";
import {
FunctionValue,
StyleValue,
toValue,
type TupleValue,
type TupleValueItem,
@@ -106,6 +107,11 @@ export const addDefaultsForTransormSection = (props: {
return setProperty("transformOrigin")(transformOrigin);
}
case "perspectiveOrigin": {
const perspectiveOrigin = parseCssValue("perspectiveOrigin", "50% 50%");
return setProperty("perspectiveOrigin")(perspectiveOrigin);
}
case "backfaceVisibility": {
return setProperty("backfaceVisibility")({
type: "keyword",
@@ -167,11 +173,14 @@ export const isTransformPanelPropertyUsed = (params: {
return currentStyle["backfaceVisibility"]?.local?.type === "keyword";
case "transformOrigin":
return currentStyle["transformOrigin"]?.local?.type === "tuple";
return currentStyle["transformOrigin"]?.local !== undefined;
case "perspective":
return currentStyle["perspective"]?.local !== undefined;
case "perspectiveOrigin":
return currentStyle["perspectiveOrigin"]?.local !== undefined;
case "rotate": {
const rotate = currentStyle["transform"]?.value;
return (
@@ -379,3 +388,27 @@ export const updateRotateOrSkewPropertyValue = (props: {
return newPropertyValue;
};
const keywordToValue: Record<string, number> = {
left: 0,
right: 100,
center: 50,
top: 0,
bottom: 100,
};
export const calculatePositionFromOrigin = (value: StyleValue | undefined) => {
if (value === undefined) {
return 50;
}
if (value.type === "unit") {
return value.value;
}
if (value.type === "keyword") {
return keywordToValue[value.value];
}
return 0;
};
@@ -45,7 +45,7 @@ import { getStyleSource } from "../../shared/style-info";
import { PropertyName } from "../../shared/property-name";
import { getDots } from "../../shared/collapsible-section";
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
import { TransformOrigin } from "./transform-origin";
import { TransformAndPerspectiveOrigin } from "./transform-and-perspective-origin";
export const transformPanels = [
"translate",
@@ -59,6 +59,7 @@ export const transformPanelDropdown = [
"backfaceVisibility",
"transformOrigin",
"perspective",
"perspectiveOrigin",
] as const;
export type TransformPanel = (typeof transformPanels)[number];
@@ -71,6 +72,7 @@ export const properties = [
"transformOrigin",
"backfaceVisibility",
"perspective",
"perspectiveOrigin",
] satisfies Array<StyleProperty>;
export const Section = (props: SectionProps) => {
@@ -88,6 +90,12 @@ export const Section = (props: SectionProps) => {
currentStyle["backfaceVisibility"]
);
const perspectiveStyleSource = getStyleSource(currentStyle["perspective"]);
const transformOriginStyleSource = getStyleSource(
currentStyle["transformOrigin"]
);
const perspectiveOriginStyleSource = getStyleSource(
currentStyle["perspectiveOrigin"]
);
const isAnyTransformPropertyAdded = transformPanels.some((panel) =>
isTransformPanelPropertyUsed({
@@ -104,6 +112,8 @@ export const Section = (props: SectionProps) => {
batch.deleteProperty("transformOrigin");
batch.deleteProperty("backfaceVisibility");
batch.deleteProperty("perspective");
batch.deleteProperty("perspectiveOrigin");
batch.publish();
};
@@ -165,7 +175,9 @@ export const Section = (props: SectionProps) => {
scaleStyleSource ||
rotateAndSkewStyleSrouce ||
backfaceVisibilityStyleSource ||
perspectiveStyleSource
perspectiveStyleSource ||
transformOriginStyleSource ||
perspectiveOriginStyleSource
}
>
{label}
@@ -192,8 +204,9 @@ export const Section = (props: SectionProps) => {
) : undefined}
<BackfaceVisibility {...props} />
<TransformOrigin {...props} />
<TransformAndPerspectiveOrigin property="transformOrigin" {...props} />
<TransformPerspective {...props} />
<TransformAndPerspectiveOrigin property="perspectiveOrigin" {...props} />
</CollapsibleSectionRoot>
);
};