microsoft / fluentui-token-pipeline

The Fluent UI token pipeline generates source code for Fluent UI libraries.
https://microsoft.github.io/fluentui-token-pipeline/
Other
41 stars 18 forks source link

Gradient exports #29

Closed TravisSpomer closed 3 years ago

TravisSpomer commented 3 years ago
TravisSpomer commented 3 years ago

Here's how you'd define a gradient brush using a fixed-pixel-size stop in XAML:

<LinearGradientBrush MappingMode="Absolute" StartPoint="0,0" EndPoint="0,4">
    <LinearGradientBrush.RelativeTransform>
        <RotateTransform Angle="180" CenterX="0.5" CenterY="0.5" />
        <!-- OR -->
        <ScaleTransform ScaleY="-1" CenterX="0.5" CenterY="0.5" />
    </LinearGradientBrush.RelativeTransform>
    <LinearGradientBrush.GradientStops>
        <GradientStop Offset="0" Color="Black" />
        <GradientStop Offset="1.0" Color="#c0c0c0" />
    </LinearGradientBrush.GradientStops>
</LinearGradientBrush>

And here's how you'd do a fixed gradient border in CSS:

.gradient-border
{
    position: relative;

    border-radius: 4px;
    background: linear-gradient(to top, black 0%, #c0c0c0 4px);
}

.gradient-inner
{
    position: absolute;
    left: 1px;
    top: 1px;
    right: 1px;
    bottom: 1px;

    border-radius: 3px;
    background-color: white;
}

In CSS, you need to use a set of nested elements: border-image can support a gradient, but not rounded corners, so the corners of the element are missing if you try to apply a rounded corner.

.gradient-border-image
{
    position: relative;

    border: 1px solid #c0c0c0;
    border-image: linear-gradient(to top, black 0%, #c0c0c0 4px);
    border-image-slice: 1;
    /* border-radius: 4px; does not help here */
    clip-path: inset(0 round 4px);
}

An alternative is to use a pseudo-element, which has the drawbacks of putting the border outside of the element, and also introduces potential z-index and layout problems.

.gradient-border-pseudo
{
    position: relative;

    border-radius: 3px;
    background-color: white;
}

.gradient-border-pseudo::before
{
    content: "";
    z-index: -1;
    position: absolute;
    left: -1px;
    top: -1px;
    right: -1px;
    bottom: -1px;

    border-radius: 4px;
    background: linear-gradient(to top, black 0%, #c0c0c0 4px);
}
TravisSpomer commented 3 years ago

Examples of potential JSON syntax:

"RegularGradient": { "value": {
  "start": [0, 1],
  "end": [0, 0],
  "stops": [
    { "position": 0, "color": "black" },
    { "position": 1, "color": "#c0c0c0" }
  ]
}},
"FixedGradient": { "value": {
  "start": [0, 1],
  "end": [0, 0],
  "stops": [
    { "position": 0, "color": "black" },
    { "position": 4, "color": "#c0c0c0" }
  ],
  "stopsUnits": "pixels"
}}

That syntax would translate to CSS trivially, with stopsUnits: "pixels" indicating that the stops.position values are in pixels rather than 0-1.

To translate the pixel stops to WinUI, when stopsUnits === "pixels": let scale be the maximum position of any gradient stop, and then divide every gradient stop by scale and add the appropriate transform to the brush.

At first, we could limit gradient support to 90-degree angles, and then arbitrary angles that would require some trig to export to CSS could come later.

Direction CSS start end
Left to right to right / 270deg 0,y 1,y
Right to left to left / 90deg 1,y 0,y
Top to bottom to bottom / 0deg x,0 x,1
Bottom to top to top / 180deg x,1 x,0

(x and y can be any number 0-1 but must be the same in both start and end)

TravisSpomer commented 3 years ago

It might be easier to decide how to represent token references once implementation starts—we'll want to be able to reuse as much of the regular alias token infrastructure as possible. color might turn into value or even an object that contains a value if that allows for more code reuse.

TravisSpomer commented 3 years ago

It's also possible that it might work better with the Figma data model if start and end are just expressed as a single angle; I'm not sure yet. The Figma API for gradients uses a matrix transform which isn't super useful on its own, but QR matrix decomposition can extract the rotation angle.

TravisSpomer commented 3 years ago

If we imagine the start and end points are on a circle, you could calculate the start and end points from an angle like this:

FromX = (sin(angle) + 1) / 2
FromY = -(cos(angle - 1) / 2
ToX = 1 - FromX
ToY = 1 - FromY

But, after some more thinking, I don't think that's what they'd expect. For a 45-degree angle, the start point would be roughly (0.85, 0.15), but it seems more likely that a person would expect (1, 0).

TravisSpomer commented 3 years ago

In general, storing the From and To points rather than just the angle seems like the most expressive way of handling things, since that captures more nuance when the angle isn't a right angle. SVG gradients do it this way as well; CSS seems to be the outlier. To get the angle from two points:

Angle = atan2(ToY - FromY, FromX - ToX) mod 360

// Or, if atan2 is not available:
Angle =
  (FromX === ToX) ?
    ((ToY > FromY) ? 0 : 180) :
    (atan((ToY - FromY) / (ToX - FromX)) + (ToX > FromX ? 270 + 90))
TravisSpomer commented 3 years ago

As always, I made a few subtle changes from my notes here in the final implementation—check the docs for the final design.