I'm Tab Atkins

and I work at Google.

@tabatkins

https://www.xanthir.com

https://www.xanthir.com/talks/2019-05-15

(Demos currently require Chrome, with the "Experimental Web Platform Features" flag turned on at chrome:flags.)

(Use left/right arrows to navigate.)

What is Houdini?

What is Houdini?

  1. Custom Properties & Values API
  2. Typed OM
  3. Custom Paint
  4. Custom Layout

Custom Properties

html {
  --header-color: #006;
  --main-color: #06c;
  --accent-color: #c06;
}
a { color: var(--main-color); }
a:visited { color: var(--accent-color); }
h1 {
  color: var(--header-color);
  background: linear-gradient(
                var(--main-color),
                #0000);
}

Custom Properties

Custom Properties originally sold as "variables" (it's in the spec name!), for organizing repeated values throughout a spec.

Actually, custom properties are an extension point — they can take anything, and be processed by JS.

However, Level 1 spec is weak, optimized for "variables" usage, and intentionally simple, for easy first implementation.

Custom Properties

Properties & Values spec starts fixing custom properties:

http://drafts.css-houdini.org/css-properties-values-api/

Custom Properties Example

Custom Properties Example

CSS.registerProperty({
  name: "--stop-color",
  syntax: "<color>",
  initialValue: "lime",
  inherits: false,
});

Punted to Level 2

Missing from current level is easy way to respond to a custom property, or changes to one.

In other words, custom properties still act like variables, not like author-created properties.

Lack is intentional, again, because simplicity makes it easier to implement correctly at first.

Level 2 will provide hooks into at least the "computed value" transformation, where most CSS magic happens.

Custom Properties 2 Example

CSS.registerProperty({
  name: "--slide-x",
  type: "<length-percentage>",
  initial: "0px",
  inherits: false
});
* {
	transform: translate(var(--slide-x), var(--slide-y));
}

Custom Properties 2 Example

CSS.registerComputedValueHook({
	inputProperties: ["transform", "--slide-*"],
	outputProperties: ["transform"],
	computedValue: function(input, output) {
	  const tx = input.get("--slide-x");
	  const ty = input.get("--slide-y");
	  const translate = new CSSTranslate(tx, ty);
	  output.set("transform", new CSSTransformValue(
		translate,
		...input.get('transform')));
	}
});

Custom Properties Example

#my-element {
	transform: rotate(10deg);
}

#my-element.foo {
	--slide-x: 5px;
	--slide-y: 10px;
}
#my-element.bar {
	--slide-x: 20px;
}

Typed OM

Previous example invoked CSSTransformValue — this is a new Typed OM value.

Current CSS "Object Model" is string-based, which is bad:

  1. Browser holds CSS values in specialized data structures.
  2. Serializes it to a string when you ask for a property value.
  3. You parse the string back into specialized JS data structures.
  4. When done, you build a string out of your values again.
  5. Browser parses the string into specialized data structures.

Real performance implications in hot code!

Typed OM

The Typed OM spec is a stopgap solution to this:

https://drafts.css-houdini.org/css-typed-om/

Creates a bunch of objects to represent CSS values, rather than strings.

Uses a Map-like interface (actually a MultiMap) for style blocks.

This is a "bedrock" spec - all the Houdini specs depend on it.

Typed OM

let width = el.computedStyleMap.get("width");
el.styleMap.set("height", width.mul(.6));
let bgs = el.styleAttributeMap.getAll("background-image");

Typed OM

In some cases, not the most convenient: "rotate(45deg)" is easier to write than new CSSRotate(CSS.deg(45))

Others similar convenience: "5px" vs CSS.px(5)

Others, much easier: rot.angle = rot.angle.add(CSS.deg(10)) is much easier than parsing the transform, altering, serializing back to string

Typed OM

Hoping for future JS improvements that would make a v2 much easier: suffix constructors (let length = 5em;), operator overloading (let newWidth = oldWidth + someLength;), etc.

Speed gains, tho, are very real — animating many transforms becomes much faster in our tests, if done properly (reusing objects).

Typed OM calc() and other math

calc(1*2 + 3) == CSSMathSum(CSSMathProduct(1,2), 3)

Typed OM math functions allow "unit algebra", dividing lengths by lengths to get a number, etc. Back-ported to normal CSS in Values & Units 4.

Also back-ported "auto-rounding" of numbers into integers when necessary.

Custom Paint

Finally getting to the actual extensions!

Custom Paint is author-defined "paint sources", usable anywhere CSS expects an <image>: background, list-style, border-image, content, etc.

https://drafts.css-houdini.org/css-paint-api/

Custom Paint Example

For example, Lea Verou's plea for conic gradients wants syntax like:

padding: 5em; /* size */
background: conic-gradient(gold 40%, #f06 0);
border-radius: 50%; /* make it round */

Custom Paint Example

CSS.registerPaint('circle', class {
	// Syntax is too complex for v1 args, so just "*" for now
	static inputArguments = ["*"];
	paint(ctx, geom, props, args) {
		// Determine the center point and cover radius.
		const x = geom.width / 2;
		const y = geom.height / 2;
		const radius = Math.hypot(x, y);

		// Draw the arcs
		let angleSoFar = CSS.deg(0);
		for(arg of args) {
			let stop = /* parse each arg into color/angle pair */;
			ctx.fillStyle = stop.color;
			ctx.beginPath();
			ctx.arc(x, y, radius, angleSoFar.value, stop.angle.value, false);
			ctx.fill();
			angleSoFar = stop.angle;
		}
	}
});

Custom Paint Example

padding: 5em; /* size */
background: paint(conic-gradient, gold 40%, #f06 0);
border-radius: 50%; /* make it round */

In future, when we have custom functions, a tiny bit more code will allow:

background: --conic-gradient(gold 40%, #f06 0);
Vincent De Oliveira's Examples

Lots more great examples

Diversion: Worklets

Custom Paint (and most other new Houdini APIs) is run in a "worklet", a lighter version of a Web Worker.

Separate process, restricted environment, might be killed and restarted at any time. Can't save state between calls.

These restrictions necessary for efficiently calling into JS in the middle of layout/painting/etc.

Custom Layout

Flexbox and Grid took so long, but we were so hungry for better layout, they're still useful.

Turnover cycle on new layout specs is way too long.

Tons more layouts we might want to add - Masonry, Tabs, Accordion, Constraint-Based...

https://drafts.css-houdini.org/css-layout-api/

Layout "below"

Existing layout modes are responsive to "natural" size of children.

This is hard to measure today in JS.

Hacks include using an off-screen iframe to measure the laid-out size of elements without disturbing the current page.

(Read the Flexbox Layout Algorithm for examples.)

Layout "above"

Existing layout modes know how to tell their parents about their own "natural" sizes, which enables auto-sizing, used everywhere.

Impossible to do well today without hacky/slow resize handlers.

(Read the CSS Sizing spec for details.)

Layout is hard

Layout is heavily optimized today, and switching between browser and JS repeatedly is expensive.

Solution, like Custom Paint, is a "worklet" for your layout code.

Custom Layout Example

Custom Layout is much more complicated and hard to write than Custom Paint.

Example I'm about to show is very preliminary, and over-simplified in dangerous ways, but shows roughly how layout code will look.

Better, more thorough examples upcoming in the spec.

Custom Layout Example

registerLayout('block-like', class {
	async layout(space, children, styleMap, breakToken) {
		let blockOffset = 0;
		const inlineSize = space.availableInlineSize;
		const childFragments = [];
		// ... snip ...
		for (let child of children) {
			let fragment = await child.doLayout(inlineSize);
			// Position the fragment in a block like manner and center
			fragment.blockOffset = blockOffset;
			fragment.inlineOffset =
				Math.max(0, (inlineSize - fragment.inlineSize) / 2);
			blockOffset += fragment.blockSize;
		}
		// ... snip ...
		return {
			blockSize: blockOffset,
			inlineSize: inlineSize,
			fragments: childFragments,
		};
	}});
Vincent De Oliveira Again!

LET'S

GET

SPECULATIVE

Custom Parser

Future Houdini specs (Custom Functions, Custom At-Rules, Custom Selectors) need to expose contents

Allowed grammars are intentionally limited; need to use "*" for more complex things

Returns "low-level" values, same as the concepts in CSS Syntax

https://wicg.github.io/CSS-Parser-API/

Custom Parser

CSS.parseRule("& .foo { new-prop: value; }");
//==>
CSSQualifiedRule({
  prelude: ["&", " ", ".", CSSKeywordValue("foo")],
  body: [CSSDeclaration({
    name: "new-prop",
    body: [CSSKeywordValue("value")])
    ]
  }
)

Custom Functions

Arbitrary "value-level" extensions

Way more complicated than they look!

darken()

CSS.registerFunction({
  "name": "--darken",
  "inputArguments": ["<color>", "<percentage>"],
  }, (color, percent) => {
    const newColor = color.toHSL();
    newColor.lightness *= (1 - percent.value/100);
    return newColor;
  });

random()

CSS.registerFunction({
  "name": "--random",
  "per": "element",
  }, () => CSS.number(Math.random()) );

Custom Functions

:root { --main-color: red; }
.foo { color: --darken(var(--main-color), 20%); }
.bar { width: calc(--random() * 100%); }

Custom Function Complications

Custom At-Rules

Arbitrary "rule-level" extensions

Typically don't rely on as much "live" information, making things easier.

@--svg

@--svg #foo {
  width: 200px; height: 100px;
  @--circle {
    cx: 50px; cy: 50px;
    r: 2em;
  }
}
.foo { background-image: --svg(#foo); }
/* ==> */
.foo { background-image: url("data:text/xml;..."); }

The End

@tabatkins

https://www.xanthir.com

https://www.xanthir.com/talks/2019-05-15