Skip to content

Commit

Permalink
Merge pull request #217 from gridaco/tokens/graphics
Browse files Browse the repository at this point in the history
Basic Background Image support
  • Loading branch information
softmarshmallow authored Jul 7, 2023
2 parents 7b4fd8f + e5eaaf2 commit 779b2cc
Show file tree
Hide file tree
Showing 43 changed files with 1,256 additions and 114 deletions.
File renamed without changes.
85 changes: 85 additions & 0 deletions docs/01-token-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Token Design Philosophy

Our token design philosophy is an essential aspect of the system that determines how we translate design inputs into rendered outputs. As we evolve our system and adapt to changes in the landscape of design and UI frameworks, we continually refine this philosophy.

## Guiding Principles

Our philosophy is underpinned by several guiding principles rooted in the field of computer science and software architecture:

- Separation of Concerns: Each token should have a specific purpose and responsibility, encapsulating a particular aspect of the design. This promotes maintainability and allows for easier understanding of the token's function.

- Encapsulation: Tokens should encapsulate both style and behavior. This ensures a comprehensive representation of UI elements and reduces complexity by providing a single point of definition for the element's look and function.

- Flexibility: The token design must be adaptable to accommodate changes in frameworks, design trends, and future growth. This agility allows the system to evolve without being overly constrained by its initial assumptions.

- Scalability: The token design should be scalable. As the system grows and accommodates more complex designs, the token structure should be capable of handling this increased complexity without a significant degradation in maintainability or performance.

## Tokenizing: Detection and Formation

Our tokenization process involves detection (recognizing UI elements) and formation (defining the structure and properties of tokens). This process converts raw input nodes into widget nodes, which are then represented as tokens. The ultimate goal is to create atomic tokens that are universally adaptable to merge and build by platforms or frameworks.

## Token Design: Property vs. Hierarchy

A key consideration when designing tokens is whether to view them as properties or as part of a hierarchy.

- Tokens as Properties: In this approach, every UI element attribute, such as color, position, or size, is seen as a separate token. This approach is quite accurate as it delineates clear boundaries between different aspects of a UI element. However, it can become noisy and complex, particularly with large UI designs.

- Tokens as Hierarchies: In this approach, tokens are seen as containers with several properties. This is an intuitive approach for developers familiar with CSS, where attributes like color, position, or opacity are properties of an element. This method can also make it easier to handle complex scenarios because related attributes are grouped together. However, this approach may also lead to over-engineering and confusion when things get more complex.

A balance between these two approaches might offer the best solution, offering a hybrid approach where some tokens are seen as properties while others are part of a hierarchy. This offers flexibility and maintainability while reducing the likelihood of overly complex structures.

## Practical Examples

To illustrate the token design philosophies, let's consider a few practical examples:

### Background Images

A background image might be represented by a single token under the "Tokens as Properties" approach, encompassing properties like image, position, scaling, and repeat. On the other hand, the "Tokens as Hierarchies" approach would represent the same as multiple tokens, each encapsulating a specific property. The hybrid approach might find a balance, potentially defining a specific token for the background image but allowing multiple properties within that token.

### Opacity

Flutter handles opacity as a widget, a unique way compared to traditional CSS-like systems. If we were to implement this in the "Tokens as Properties" approach, we might allow an 'opacity' property within our generic tokens, much like CSS. In the "Tokens as Hierarchies" approach, we would create a specific 'opacity' token, much like Flutter's widget approach. The hybrid approach might define 'opacity' as a property for most tokens but allow it to be a token on its own in more complex scenarios.

## Token Design Checklist

Before finalizing a token design, we go through the following checklist to ensure it aligns with our principles and will work with our system:

- **Representability**: Can it be represented without reverse conversion for all platforms?

- **Documentation**: Does the concept exist and have well-documented references?

- **Extensibility**: At the point when the architecture needs to be changed, is it possible to double tokenize instead of rewriting (with all related concepts)?

- **Compatibility**: Is it representable by pure CSS? Is it representable by pure Flutter? How much compatibility does it have with graphics libraries, e.g., Skia?

## Tokens

We classify tokens into several categories based on their purpose and properties, some of which include layouts, containers, and positioning/sizing.

- **Layouts**: Column, Row, Grid, Stack, Table, ListView.

- **Containers**: Container token can represent a UI element that houses other elements.

- **Positioning / Sizing**: Align, Center, SizedBox, Expanded, Transform.

Each of these categories has a unique role to play and contains tokens that reflect that role. The design of these tokens follows the guiding principles and considerations we have outlined above.

## Considerations for Adding New Framework Support

When adding support for a new framework, the token design philosophy should guide the approach:

1\. **Understand the Framework**: Analyse the framework to understand its principles, idioms, and quirks. This understanding will help identify how it aligns with or diverges from the token design philosophy.

2\. **Map Existing Tokens**: Identify how existing tokens can be represented in the new framework. Some tokens may directly map to concepts in the framework, while others may need a more nuanced approach.

3\. **Identify Gaps**: Identify any concepts or features in the new framework that cannot be represented with existing tokens. This will help in identifying new tokens that need to be created.

4\. **Create New Tokens**: Create new tokens as necessary to represent unique features or concepts in the new framework. These tokens should follow the token design philosophy.

5\. **Optimize**: Once the initial mapping is complete, optimize for performance and maintainability. Look for ways to simplify the representation or improve the efficiency of rendering.

## Wrapping Up

In conclusion, our token design philosophy aims to create a balance between accuracy, intuitiveness, flexibility, and scalability. We look for ways to encapsulate UI elements as tokens, following a hybrid approach of considering tokens as both properties and hierarchies. With these guiding principles, our tokens should be versatile enough to represent design inputs across various frameworks and adaptable enough to evolve with the changing landscape of UI design and development.

By sticking to this philosophy, we set ourselves up for success in developing a system that not only efficiently interprets design inputs into renderable outputs but also is maintainable, extensible, and scalable, allowing us to continually adapt and grow with the ever-evolving UI design and development industry.
17 changes: 17 additions & 0 deletions docs/background-multiple-images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Multiple background images

There are mainly two factors to consider, one being the transform, second being number of backgrounds.

The interpretation of multiple background image data vary by the target frameworks.

For instance in CSS, you can use multiple background images by separating them with comma, but in Flutter, you can't.

Even this vary in the same framework, if the image has a transform applied to it.

For more info about how we decided to handle multiple backgroun images in each framework please read the following articles.

- [css-multiple-background](./css-multiple-background.md)

## Related articles

- [MDN - Using Multiple Backgrounds](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Backgrounds_and_Borders/Using_multiple_backgrounds)
51 changes: 51 additions & 0 deletions docs/background.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# background (fills)

Background handling is quite straightforward, but there are some edge cases that we need to consider.

## Considerations

### A. Multiple Backgrounds

While CSS supports having multiple background in a single widget, Flutter doesn't.

### B. Mixed Types

### C. Background Image `transform`

Neither CSS nor Flutter supports applying `transform` to a background image. We have to transform the widget tree to properly render it, wrapping the ImageWidget with .

### D. Video Background Image

Using Image as a background in a single widget is not supported in all major frameworks. we have to Transform the widget tree to stack the video as a background to properly render it.

### E. Tree Transformation

### Related Documents

- [Multiple Background Images](./background-multiple-images.md)

## Tokenization

Factors to consider

- does background image has opacity?
- does background image has transform?
- does background image has filter?
- does background image has rotation?
- does background image has scale factor for tile mode?

| Count | Children (n) | Type | Has Image | Has Video | Image Transform | Image Filter | Composition | Token Tree | option | Notes |
| ----- | ------------ | ------------ | --------- | --------- | --------------- | ------------ | ----------------------------- | ------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0` | `*` | - | - | - | - | - | - | - | - | - |
| `1` | `*` | `color` | - | - | - | - | `background-color(node)` | `Container()` | - | If node has single fill with color, use standard background color handling |
| `1` | `*` | `gradient` | - | - | - | - | `background-gradient(node)` | `Container()` | - | If node has single fill with gradient, use standard background color handling |
| `1` | `*` | `image` | `true` | - | `false` | `false` | `background-image(node)` | `Container()` | - | If node has single fill with image, use standard background image handling |
| `1` | `0` | `image` | `true` | - | `false` | `false` | `image()` | `Image()` | `{ use_image_widget_if_possible: true }` | Optionally, If node has single fill with image, with no children, no filter nor transform, it can be transformed to image widget |
| `1` | `*` | `video` | - | `true` | - | - | `background-video` | `Stack(Video(), Container())` | - | If node has single fill with video, use tree transformation to put video under new `Stack` as `Video` along with the node itself as a `Container` |
| `>1` | `*` | `color[]` | - | - | - | - | `[background-color(node)]` | `Container(...)` | - | - |
| `>1` | `*` | `gradient[]` | - | - | - | - | `[background-gradient(node)]` | `Container(...)` | - | - |
| `>1` | `*` | `image[]` | `true` | - | `false` | `false` | `[background-image(node)]` | `Container(...)` | - | - |
| `>1` | `0` | `image[]` | `true` | - | `false` | `false` | `Stack(image()[])` | `Image()` | `{ use_image_widget_if_possible: true }` | Optionally, If node has multiple image fills, with no children, no filter nor transform, it can be transformed to image widget |
| `>1` | `*` | `video[]` | - | `true` | - | - | `background-video-many` | `Stack(Video()[], Container())` | - | If node has multiple video fills, use tree transformation to put videos under new `Stack` as `Video[]` along with the node itself as a `Container` |
| `>1` | `*` | `mixed` | `false` | `false` | - | - | `background(node)` | `Container(...)` | - | If node has multiple mixed fills, but not image or video, use the standard background-image and background-gradient |
| `>1` | `*` | `mixed` | `false` | `false` | - | - | `background(node)` | `Container(...)` | - | If node has multiple mixed fills, but not image or video, use the standard background-image and background-gradient |
94 changes: 94 additions & 0 deletions docs/figma-image-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
stage: draft
note: image with image filter applyed is not supported atm. see the reason below
---

# Image Filters in Figma and how we handle it

> Note: The Figma's Image Filter implementation is custom and not directly compatible with Image processing standards like CSS Filter. For the accuracy, we have to use exported node image to render this visually as-is. (Yet, exporting image applys to whole node, we can't handle individual fills - it would bake the entier node including other properties and children)
The porperties of `ImageFilters` are,

- `exposure` - _similar to `brightness` in CSS Filter_
- `contrast` - _similar to `contrast` in CSS Filter_
- `saturation` - _similar to `saturate` in CSS Filter_
- `temperature` - _similar to `hue-rotate` in CSS Filter_
- `tint` - _similar to `hue-rotate` in CSS Filter_
- `highlights` - _similar to `brightness` in CSS Filter_
- `shadows` - _similar to `brightness` in CSS Filter_

| Property | CSS | Flutter |
| ----------- | -------------------------- | ---------------------------------------------------------------------------------------- |
| exposure | \*`brightness`, `contrast` | N/A |
| contrast | \*`contrast` | `ColorFilter` |
| saturation | `saturate` | `ColorFilter.matrix([1.5, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 1, 0])` |
| temperature | N/A | `ColorFilter.matrix([1.5, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 1, 0])` |
| tint | N/A | `ColorFilter.matrix([1.5, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 1, 0])` |
| highlights | N/A | `ColorFilter.matrix([1.5, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 1, 0])` |
| shadows | N/A | `ColorFilter.matrix([1.5, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 1, 0])` |

## CSS

CSS Filter mapped to Figma's Image Filter

| CSS Filter | Figma's Image Filter |
| ------------- | -------------------------- |
| `blur` | N/A (available ine ffects) |
| `brightness` | `exposure` |
| `contrast` | `contrast` |
| `drop-shadow` | |
| `grayscale` | |
| `hue-rotate` | |
| `invert` | |
| `opacity` | |
| `saturate` | `saturation` |
| `sepia` | |

**`Fimga#ImageFilter#exposure`**

```css
/* Exposure doesn't have a direct equivalent in CSS,
but we can use brightness and contrast to mimic this.
Since CSS doesn't accept negative values for these functions,
we'll use a value of 0.5 to represent -1, 1 to represent 0,
and 1.5 to represent 1. */
img {
filter: brightness(0.5) contrast(1.5); /* -1 in Figma */
}
img {
filter: brightness(1) contrast(1); /* 0 in Figma */
}
img {
filter: brightness(1.5) contrast(0.5); /* 1 in Figma */
}
```

**`Fimga#ImageFilter#contrast`**

```css
/* Contrast in CSS */
img {
filter: contrast(0); /* -1 in Figma */
}
img {
filter: contrast(1); /* 0 in Figma */
}
img {
filter: contrast(2); /* 1 in Figma */
}
```

**`Fimga#ImageFilter#saturation`**

```css
/* Saturation in CSS */
img {
filter: saturate(0); /* -1 in Figma */
}
img {
filter: saturate(1); /* 0 in Figma */
}
img {
filter: saturate(2); /* 1 in Figma */
}
```
1 change: 1 addition & 0 deletions editor/next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const IS_DEV = process.env.NODE_ENV === "development";

const packages = [
"@engine/core",
// region @editor-app
"@code-editor/analytics",
"@code-editor/ui",
Expand Down
2 changes: 1 addition & 1 deletion editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@emotion/react": "^11.11.0",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@figma-api/community": "^0.0.5",
"@figma-api/community": "^0.0.7",
"@floating-ui/core": "^1.0.1",
"@floating-ui/react-dom": "^1.0.0",
"@floating-ui/react-dom-interactions": "^0.10.2",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"resolutions": {
"@design-sdk/figma-node": "0.0.25",
"@reflect-ui/core": "0.0.9",
"@reflect-ui/core": "0.0.12",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@types/react": "18.0.24",
Expand Down
Loading

1 comment on commit 779b2cc

@vercel
Copy link

@vercel vercel bot commented on 779b2cc Jul 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.