Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

feat(shape): Adding MDCShape, an experimental subsystem #1448

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions demos/shape/heart/foundation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import MDCShapeFoundation from '../../../packages/mdc-shape/foundation';

export default class DemoHeartFoundation extends MDCShapeFoundation {
generatePath_(width, height, padding) {
return 'm ' + (width / 2) + ' ' + (height - padding)
+this.generateLeftBase_(width, height, padding)
+this.generateTopLeftCurve_(width, height, padding)
+this.generateTopRightCurve_(width, height, padding)
+this.generateRightBase_(width, height, padding);
Copy link
Contributor

Choose a reason for hiding this comment

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

If we are adding a space after the operator elsewhere, we should do the same here.

}
generateBaseWidth_(width, padding) {
return ((width - (padding * 2)) / 2);
Copy link
Contributor

Choose a reason for hiding this comment

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

parens around padding * 2 is unnecessary.

}
generateBaseFlatWidth_(width, padding) {
return this.generateBaseWidth_(width, padding) * 0.14;
}
generateBaseCurveWidth_(width, padding) {
return this.generateBaseWidth_(width, padding) * 0.52;
}
generateBaseHeight_(height, padding) {
return (height - (padding * 2)) * 0.67;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment, remove parens around padding * 2

}
generateBaseFlatHeight_(height, padding) {
return this.generateBaseHeight_(height, padding) * 0.11;
}
generateBaseCurveHeight_(height, padding) {
return this.generateBaseHeight_(height, padding) * 0.4;
}
generateBaseCurveHeight2_(height, padding) {
return this.generateBaseHeight_(height, padding) * 0.67;
}
generateLeftBase_(width, height, padding) {
const flatWidth = this.generateBaseFlatWidth_(width, padding);
const flatHeight = this.generateBaseFlatHeight_(height, padding);
const realWidth = this.generateBaseWidth_(width, padding) - flatWidth;
return 'l ' + (-1 * flatWidth) + ',' + (-1 * flatHeight)
+'c ' + (-1 * this.generateBaseCurveWidth_(width, padding)) + ','
+ (-1 * this.generateBaseCurveHeight_(height, padding))
+ ' ' + (-1 * realWidth) + ',' + (-1 * this.generateBaseCurveHeight2_(height, padding))
+ ' ' + (-1 * realWidth)
+ ',' + (-1 * (this.generateBaseHeight_(height, padding) - flatHeight));
}
generateRightBase_(width, height, padding) {
const flatWidth = this.generateBaseFlatWidth_(width, padding);
const flatHeight = this.generateBaseFlatHeight_(height, padding);
const curveHeight = this.generateBaseHeight_(height, padding) - flatHeight;
const realWidth = this.generateBaseWidth_(width, padding) - flatWidth;
return 'c 0,' + (curveHeight - this.generateBaseCurveHeight2_(height, padding))
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like a good place to use template strings in JS to make this return statement a bit simpler.

+ (-1 * (realWidth - this.generateBaseCurveWidth_(width, padding)))
+ ',' + (curveHeight - this.generateBaseCurveHeight_(height, padding))
+ (-1 * realWidth) + ',' + curveHeight
+'l ' + (-1 * flatWidth) + ',' + flatHeight;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: space after first + operator.

Copy link
Contributor

Choose a reason for hiding this comment

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

Another nit... do we not prefer trailing operators rather than leading? (Maybe moot if we reorganize this and use a template string.)

Copy link
Contributor

Choose a reason for hiding this comment

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

My Outlined Text Field prototype uses leading operators, and the king of SVG on the web Mike Bostock always uses leading operators. I am open to more clearly defining our convention here, and am not married to leading operators.

Copy link
Contributor

@kfranqueiro kfranqueiro Oct 19, 2017

Choose a reason for hiding this comment

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

I guess it comes down to whether we want to adhere to Google's JS style guide for this, which promotes breaking after the operator, not before.

If we want to enforce this, we can use https://eslint.org/docs/rules/operator-linebreak

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the closer we are to the Google style guide the better. Since that is the companies encouraged policy on coding style.

However, I still think shifting these to template literals would net a simpler codebase to maintain long-term.

}
generateOutsideWidth_(width, padding) {
return ((width - (padding * 2)) / 2) * 0.55;
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove parens around padding * 2

}
generateOutsideCurveWidth_(width, padding) {
return this.generateOutsideWidth_(width, padding) * 0.44;
}
generateInsideWidth_(width, padding) {
return ((width - (padding * 2)) / 2) * 0.45;
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove parens around padding * 2

}
generateInsideCurveWidth_(width, padding) {
return this.generateInsideWidth_(width, padding) * 0.37;
}
generateInsideCurveWidth2_(width, padding) {
return this.generateInsideWidth_(width, padding) * 0.76;
}
generateTopHeight_(height, padding) {
return (height - (padding * 2)) * (1 - 0.67);
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove parens around padding * 2

}
generateTopCurveHeight_(height, padding) {
return this.generateTopHeight_(height, padding) * 0.56;
}
generateDipHeight_(height, padding) {
return (height - (padding * 2)) * 0.15;
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove parens around padding * 2

}
generateDipCurveHeight_(height, padding) {
return this.generateDipHeight_(height, padding) * 0.38;
}
generateTopLeftCurve_(width, height, padding) {
const topHeight = this.generateTopHeight_(height, padding);
return 'c 0,' + (-1 * this.generateTopCurveHeight_(height, padding)) + ' '
+ this.generateOutsideCurveWidth_(width, padding) + ',' + (-1 * topHeight) + ' '
+ this.generateOutsideWidth_(width, padding) + ',' + (-1 * topHeight)
+'c ' + this.generateInsideCurveWidth_(width, padding) + ',0, '
+ this.generateInsideCurveWidth2_(width, padding)
+ ',' + (this.generateDipCurveHeight_(height, padding)) + ', '
+ this.generateInsideWidth_(width, padding) + ',' + this.generateDipHeight_(height, padding);
}
generateTopRightCurve_(width, height, padding) {
const outsideWidth = this.generateOutsideWidth_(width, padding);
const insideWidth = this.generateInsideWidth_(width, padding);
const topHeight = this.generateTopHeight_(height, padding);
const dipHeight = this.generateDipHeight_(height, padding);
return 'c ' + (insideWidth - this.generateInsideCurveWidth2_(width, padding))
+ ',' + (-1 * (dipHeight - this.generateDipCurveHeight_(height, padding))) + ' '
+ (insideWidth - this.generateInsideCurveWidth_(width, padding))
+ ',' + (-1 * dipHeight) + ' '
+ insideWidth + ',' + (-1 * dipHeight)
+'c ' + (outsideWidth - this.generateOutsideCurveWidth_(width, padding)) + ',0 '
+ outsideWidth + ',' + (topHeight - this.generateTopCurveHeight_(height, padding)) + ' '
+ outsideWidth + ',' + topHeight;
}
}
33 changes: 33 additions & 0 deletions demos/shape/heart/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import DemoHeartFoundation from './foundation';
import MDCShape from '../../../packages/mdc-shape/component';

export default class DemoHeart extends MDCShape {
static attachTo(root) {
return new DemoHeart(root);
}

getDefaultFoundation() {
return new DemoHeartFoundation(this.createAdapter());
}

initialSyncWithDOM() {
super.initialSyncWithDOM();
this.foundation_.redraw();
}
}
104 changes: 104 additions & 0 deletions demos/shape/shape.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<!DOCTYPE html>
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Rename this to shape/index.html?

<!--
Copyright 2016 Google Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
-->
<html class="mdc-typography">
<head>
<meta charset="utf-8">
<title>Shape - Material Components Catalog</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/images/logo_components_color_2x_web_48dp.png">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Mono">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<script src="/assets/shape/shape.css.js"></script>
</head>
<body>
<header class="mdc-toolbar mdc-toolbar--fixed">
<div class="mdc-toolbar__row">
<section class="mdc-toolbar__section mdc-toolbar__section--align-start">
<span class="catalog-back">
<a href="/" class="mdc-toolbar__icon--menu"><i class="material-icons">&#xE5C4;</i></a>
</span>
<span class="mdc-toolbar__title catalog-title">Shape</span>
</section>
</div>
</header>
<main>
<div class="mdc-toolbar-fixed-adjust"></div>
<section class="hero">
<div class="mdc-shape">
<canvas class="mdc-shape__canvas"></canvas>
<svg class="mdc-shape__svg">
<clipPath>
<path class="mdc-shape__path"/>
</clipPath>
</svg>
</div>
</section>
<section>
<fieldset>
<legend class="mdc-typography--title">Change Elevation</legend>
<figure>
<div class="mdc-shape" id="elevatable-shape">
<canvas class="mdc-shape__canvas"></canvas>
<svg class="mdc-shape__svg">
<clipPath>
<path class="mdc-shape__path"/>
</clipPath>
</svg>
</div>
</figure>
<figcaption class="demo-input">
<span>Elevation</span>
<input type="number" name="elevation-input" value="4" id="elevation-input">
</figcaption>
</fieldset>
</section>
</main>
<script src="/assets/material-components-web.js"></script>
<script src="/assets/shape/heart/index.js"></script>
<script>
// Because we load our CSS via webpack, we need to ensure that all of the correct styles
// are applied before ripples are attached. Otherwise, ripples may use the computed styles of
// elements before our CSS is applied, leading to improper UX.
(function() {
var pollId = 0;
pollId = setInterval(function() {
var pos = getComputedStyle(document.querySelector('.mdc-shape')).position;
if (pos === 'relative') {
init();
clearInterval(pollId);
}
}, 250);
function init() {
const DemoHeart = mdc["shape/heart/index"].default;
var shapes = document.querySelectorAll(".mdc-shape");
let elevatableHeart;
for (var i=0; i<shapes.length; i++) {
const demoHeart = DemoHeart.attachTo(shapes[i]);
if (shapes[i].id === "elevatable-shape") {
elevatableHeart = demoHeart;
}
}
const elevationInput = document.querySelector("#elevation-input");
elevationInput.addEventListener("change", () => {
elevatableHeart.elevation = elevationInput.value;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm noticing that there is sometimes visible jank when changing this elevation and I'm not sure whether it's a flicker of the clip-path resetting or what.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There was a bug with parsing the elevation value into an int, this was causing the penumbra shadow to draw in the wrong place. Fixed the bug and now I see now jank.

FYI, the clip-path doesn't change when the elevation changes .... but the entire canvas redraws. Its not GPU accelerated, but thats kinda the deal with shapes.

});
}
})();
</script>
</body>
</html>
22 changes: 22 additions & 0 deletions demos/shape/shape.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

@import "../common";
@import "../../packages/mdc-shape/mdc-shape";

fieldset {
margin: 24px;
margin-top: 0;
margin-bottom: 16px;
}
.mdc-shape {
--mdc-shape-elevation: 4;
--mdc-shape-background: #FF00FF;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit (here and anywhere else applicable): lowercase hex values

width: 100px;
height: 100px;
margin: 16px auto;
}
.demo-input, .demo-button {
text-align: center;
}
#heart-ripple {
height: 100%;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
"rtl",
"select",
"selection-control",
"shape",
"slider",
"snackbar",
"switch",
Expand Down
100 changes: 100 additions & 0 deletions packages/mdc-shape/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Shape

MDC Shape is a Sass / CSS / JavaScript library which draws shapes.

## Design & API Documentation

TODO

## Installation

TODO

## Usage

### DOM Structure

```html
<div class="mdc-shape">
<canvas class="mdc-shape__canvas"></canvas>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this a canvas and does it always have to be? Is shape expected to eventually work on other components?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does always have to be a canvas.

Initially we had an implementation that clipped divs with clip-path, then applied a drop-shadow filter. This has a couple performance problems

  • Calculating drop-shadow requires iterating over every pixel within that div. So the larger the div the slower the performance
  • The drop-shadow is repainted (and therefore recalculated) whenever the content underneath the clipped div changed. (Technically, whenever any content in the same GPU tile changes.) This means if you were scrolling content under the clipped div, the scroll could become janky.

Also for working with other components...yes some day. Since this is still experimental we've only built some offhand prototypes (like for button). In the future most components will probably have some "shape" version, in addition to the default version, which has a modified HTML structure to include canvas. Obviously, these will be very heavy weight components and should be used with discretion.

<div style="clip-path: url(#FOO_ID); -webkit-clip-path: url(#FOO_ID)">Your Content</div>
<svg class="mdc-shape__svg">
<clipPath id="FOO_ID">
<path class="mdc-shape__path"/>
</clipPath>
</svg>
</div>
```

We recommend you put any content for the shape inside the "clipped" element *after* the `mdc-shape__canvas` element. The "clipped" element must contain the style `clip-path:url(#FOO_ID)`, where `FOO_ID` corresponds to the value you assign to the `clipPath` element's `id` attribute. Using the same ID between the "clipped" element and the `clipPath` element effectively clips any content to the shape. It is important `mdc-shape__canvas` is before the "clipped" element, otherwise the clipping effect will clip the shadows drawn by `MDCShapeFoundation`.

Copy link
Contributor

Choose a reason for hiding this comment

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

We are using the word clip a lot... Lets workshop some ideas to make this read easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

any suggestions? Not sure which direction you want to take this... "cropped" or "limited to this area covered by svg"?

Copy link
Contributor

Choose a reason for hiding this comment

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

Clipped seems like an appropriate word to me, but maybe introduce the vocabulary by outlining the purpose that each of the 3 elements serves in a bulleted list, then we don't have to air-quote "clipped" everywhere?

### CSS Classes

CSS Class | Description
--- | ---
`mdc-shape` | Mandatory. Needs to be set on the root element of the component
`mdc-shape__canvas` | Mandatory. Needs to be set on the canvas node for drawing the shape
`mdc-shape__svg` | Mandatory. Needs to be set on the svg node for clipping content to the shape
`mdc-shape__path` | Mandatory. Needs to be set on the path node for clipping content to the shape

### Using the Foundation Class

MDC Shape ships with an `MDCShapeFoundation` class that external frameworks and libraries can use to integrate the component. As with all foundation classes, an adapter object must be provided.
The adapter for shape must provide the following functions, with correct signatures:

| Method Signature | Description |
| --- | --- |
| `setCanvasWidth(value: number) => void` | Sets the width of the canvas element. |
| `setCanvasHeight(value: number) => void` | Sets the height of the canvas element. |
| `getCanvasWidth() => number` | Returns the width of the canvas element. |
| `getCanvasHeight() => number` | Returns the height of the canvas element. |
| `getDevicePixelRatio() => number` | Returns the device pixel ratio. |
| `create2dRenderingContext() => {shadowColor: string, shadowBlur: number, shadowOffsetY: number, fillStyle: string, scale: (number, number), clearRect: (number, number, number, number), fill: (Path2D)}` | Returns an object which has the shape of a CanvasRenderingContext2d instance. An easy way to achieve this is simply `this.root_.querySelector(mdc.shape.MDCShapeFoundation.SHAPE_SELECTOR).getContext('2d');`. |

Copy link
Contributor

Choose a reason for hiding this comment

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

This line makes the table scroll horizontally because the code snippet in the second column will not wrap. No bueno.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved it down to another section, hopefully that reads better

### Extending the Foundation Class

`MDCShapeFoundation` is an abstract class. Developers should extend `MDCShapeFoundation` and implement the `generatePath_` method. GeneratePath_ takes width, height, and padding, and returns a string representation of the SVG path data. This allows developers to create *any* shape.

TODO add more information about the other shapes we provide, which extend MDCShapeFoundation.

### MDCShape API

MDC Shape exposes the following methods:

| Method Signature | Description |
| --- | --- |
| `set background(value: string) => void` | Sets the background of the shape |
| `set elevation(value: number) => void` | Sets the elevation of the shape |
| `redraw() => void` | Redraws the shape |

### Shape Customization

There are two ways to customize your shape's elevation and background color. The first way is to use the MDCShape's API.

To modify the elevation of a shape, call the background setter

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know if this is the best language. Maybe just "...shape, set the background"?

```
mdcShape.background = '#FOO';
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: lowercase f in the color hex value

```

To modify the elevation of a shape, call the elevation setter

Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above. I don't know if "call a setter" is the right vocab.

```
mdcShape.elevation = 4;
```

The second way is to use custom CSS properties.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we really expose 2 ways to do the same thing, especially if the 2nd way won't work in all browsers? (I have more thoughts on the CSS custom properties approach in another comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair enough, I removed the custom CSS properties approach.


### CSS custom properties

To modify the elevation of a shape, set custom CSS properties on the mdc-shape element

```
--mdc-shape-elevation: 4;
```

To modify the background of a shape, set custom CSS properties on the mdc-shape element

```
--mdc-shape-background: #FF0000;
```
Loading