Houdini — CSS, который JavaScript

Никита Дубко

Никита Дубко

Houdini —
CSS, который JavaScript

Никита Дубко

MinskCSS MinskJS CSS-Minsk-JS Никита Дубко
CSS-Minsk-JS css-minsk-js.by WeLoveFrontTalks

Конвейер рендеринга в браузере

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript
  2. Построение Document Object Model и CSS Object Model

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript
  2. Построение Document Object Model и CSS Object Model
  3. Формирование дерева рендеринга

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript
  2. Построение Document Object Model и CSS Object Model
  3. Формирование дерева рендеринга
  4. Расчет положения на странице каждого элемента дерева рендеринга

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript
  2. Построение Document Object Model и CSS Object Model
  3. Формирование дерева рендеринга
  4. Расчет положения на странице каждого элемента дерева рендеринга
  5. Отрисовка пикселей каждого слоя

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript
  2. Построение Document Object Model и CSS Object Model
  3. Формирование дерева рендеринга
  4. Расчет положения на странице каждого элемента дерева рендеринга
  5. Отрисовка пикселей каждого слоя
  6. Компоновка слоев и отображение в видимой области браузера

Конвейер рендеринга в браузере

  1. Магия 🔮
  2. Можем влиять на процесс из JavaScript 💪
  3. Магия 🔮
  4. Магия 🔮
  5. Магия 🔮
  6. Магия 🔮

JavaScript

Полифил — это код, реализующий какую-либо функциональность, которая не поддерживается в некоторых версиях веб-браузеров по умолчанию.

Полифил

(function(self) {
    'use strict';
    if (self.es2020feature) {
        return;
    }
    self.es2020feature = function() {
	      // Do some magic
    };
    self.es2020feature.polyfill = true;
})(this);

Понифил 🦄

ponyfill.com

// is-nan-ponyfill.js
module.exports = function (value) {
	return value !== value;
};

// app.js
var isNanPonyfill = require('is-nan-ponyfill');
isNanPonyfill(5);

Транспайлер

// ES6
let point = { x: 0, y: 0 };
const {x, y} = point;
const arrowFunction = (a) => a ** 2;
// ES5
'use strict';
var point = { x: 0, y: 0 };
var x = point.x,
    y = point.y;
var arrowFunction = function arrowFunction(a) {
  return Math.pow(a, 2);
};

CSS

Ошибки в проектировании CSS

LESS SASS Stylus PostCSS

Polyfill.js 🎉

Polyfill({
		declarations:["position:sticky"]
	})
	.doMatched(function(rules) {
		/* add styles */
	 })
	.undoUnmatched(function(rules) {
		/* reset styles */
	});
Polyfill.js author's message
Sad

CSS-TAG Houdini Task Force 💎

CSS-TAG Houdini Task Force
Гарри Гудини
Новые спецификации Houdini
Новые спецификации Houdini для работы с конвейером рендеринга
CSS Houdini specifications

Репозиторий проекта на Github

CSS Custom Properties

:root {
	--r: 255;
	--g: 20;
	--b: 147;
	--primary-color: rgb(var(--r), var(--g), var(--b));
}

.my-element {
	--e-width: 200px;
	--e-border-width: 20px;
	background-color: var(--primary-color);
	width: calc(var(--e-width) + var(--e-border-width));
}
const isSupported = CSS.supports('--custom', 'property');
// Получить значение custom property
const pinkElement = document.querySelector('.pink-element');
const elementWidth = window
   .getComputedStyle(pinkElement)
   .getPropertyValue('--element-width');
// Изменить custom property для узла
pinkElement.style.setProperty('--element-width', '300px');
// Изменить custom property в :root
document.documentElement.style.setProperty('--r', 190);
Custom Properties — caniuse.com

демо Stargate

chrome://flags

Experimental flag

CSS Typed OM

Проблема #1

const fontSize = window
	.getComputedStyle(elem)
	.getPropertyValue('font-size');
const fontSizeValue = parseFloat(fontSize);
console.log(fontSizeValue); // 25

Решение #1

const element = document.querySelector('.elem');

const styleMap = element.attributeStyleMap;
console.log( styleMap.get('font-size') );
// CSSUnitValue {value: 2.5, unit: "em"}
const computedStyleMap = element.computedStyleMap();
console.log( computedStyleMap.get('font-size') );
// CSSUnitValue {value: 25, unit: "px"}

Проблема #2

const x = 50;
const offset = 10;
element.style.setProperty(
	'transform',
	`translate(${x}px, calc(1em + ${offset}px))`
);

Решение #2

const styleMap = element.attributeStyleMap;
const transformValue = new CSSTransformValue([
	new CSSTranslate(
		CSS.px(50),
		new CSSMathSum(CSS.em(1), CSS.px(5))
	)
]);
styleMap.set('transform', transformValue);

Отлов ошибок

try {
	CSSStyleValue.parse(
		'transform',
		'translate4d(bogus value)'
	);
} catch (err) {
	console.err(err);
}

Иерархия возможных значений свойств

Проверить поддержку

if (window.CSS && CSS.number) {
	// В вашем браузере CSS Typed OM работает
}
		

полифил typed-om

полифил typed-om

Chrome 66+

Где почитать?

CSS Parser API

Идея

const background = window.cssParse.rule("background: green");
background.attributeStyleMap.get("background").value; // "green"

const styles = window.cssParse.ruleSet(
	`.foo {
		background: green;
		margin: 42px;
	}`
);
styles.length; // 5
styles[0].attributeStyleMap.get("margin-top").value; // 42

Асинхронная загрузка

const style = fetch("style.css")
	.then(response => {
		const styles = CSS.parseStylesheet(response.body);
		// применить PostCSS прямо в браузере
		return styles;
	});
style.then(console.log);

CSS Properties and Values API

Проблема #3

:root {
	--sidebar-width: 400px;
}
.closed {
	--sidebar-width: 80px;
}
body {
	transition: --sidebar-width 1s;
}
// JavaScript
CSS.registerProperty({
	name: '--sidebar-width',
	syntax: '<length>',
	inherits: true,
	initialValue: '80px'
});
/* CSS */
@property --sidebar-width {
	syntax: "<length>",
	inherits: true,
	initialValue: "80px"
}

Синтаксис

<length>
<number>
<percentage>
<length-percentage>
<color>
<image>
<url>
<integer>
<angle>
<time>
<resolution>
<transform-function>
<custom-ident>

<length | number>
<length>+
<image>#
small | smaller

Уже применяют

Демо Mebius

Chrome Canary *

Observer

Worklets

// app.js — псевдокод
window.someWorklet
	.addModule('some-worklet.js')
	.then(_ => {
		console.log('some-worklet — loaded');
	});

// some-worklet.js — псевдокод
registerSomeWorklet('some-worklet-name', class {
	process(arg) {
		// делаем магию
	}
});
Много потоков

Загрузка через Blob. Шаг 1

<script language="javascript+paint">
	registerPaint('circle', class {
		paint(ctx, geom, properties) {
			// worklet implementation
		}
	});
</script>

Загрузка через Blob. Шаг 2

<script>
	function blobWorklet() {
		const src = document
			.querySelector('script[language$="paint"]')
			.innerHTML;
		const blob = new Blob([src], {type: 'text/javascript'});
		return URL.createObjectURL(blob);
	}
	if ('paintWorklet' in CSS) {
		CSS.paintWorklet.addModule(blobWorklet());
	}
</script>

CSS Painting API 🎨

Демо image-placeholder

CSS Painting API

Можно применять для отрисовки свойств:

// my-paint.js
registerPaint('my-paint', class MyPaint {
	static get inputProperties() { return ['--foo']; }
	static get inputArguments() { return ['<color>']; }
	static get contextOptions() { return { alpha: true }; }

	paint(ctx, geom, properties, args) {
		// ctx - контекст для рисования, как в canvas
		// geom - размеры доступной для рисования области
		// properties - свойства, на которые реагирует paintWorklet
		// args - переданные в paintWorklet аргументы
		// Можно рисовать почти как на обычном canvas
	}
});
/* style.css */
.my-element {
	--foo: deeppink;
	background-image: paint(my-paint);
}

// app.js
CSS.paintWorklet.addModule('my-paint.js');

Ограничения контекста для Paint Worklet

Chrome 65+

css-paint-polyfill

Box Tree API



<style>
p::first-line { color: green; }
p::first-letter { color: red; }
</style>

<p>foo <i>bar baz</i></p>

foo bar baz

const element = document.querySelector('.my-element');
element
	.getFragmentInformation("direct-fragments-only")
	.then(info => {
		// ...
	});

// Значения фильтра
"direct-fragments-only" | "fragment-hierarchy"
interface DeadFragmentInformation {
	Node node;
	double width;
	double height;
	double top;
	double left;
	boolean isOverflowed;
	FrozenArray<DeadFragmentInformation>? children;
	DeadFragmentInformation? nextSibling;
	DeadFragmentInformation? previousSibling;
	DeadFragmentInformation? nextInBox;
	DeadFragmentInformation? previousInBox;
};

CSS Layout API 🚀

Masonry

Демо masonry

registerLayout('my-layout', class {
	static get inputProperties() { return ['--foo']; }
	static get childrenInputProperties() { return ['--bar']; }
	static get layoutOptions() {
		return {childDisplay: 'normal', sizing: 'block-like'};
	}

	*intrinsicSizes(children, edges, styleMap) {
		// Вычислить размеры элемента
	}

	*layout(children, edges, constraints, styleMap, breakToken) {
		// Разместить всех children
	}
});

*intrinsicSizes

dictionary IntrinsicSizesResultOptions {
	double maxContentSize;
	double minContentSize;
};

*layout

dictionary FragmentResultOptions {
	double inlineSize = 0;
	double blockSize = 0;
	double autoBlockSize = 0;
	sequence<LayoutFragment> childFragments = [];
	any data = null;
	BreakTokenOptions breakToken = null;
};
/* CSS */
.layout-example {
	--foo: 5;
	display: layout(my-layout);
}

.layout-example .child {
	--bar: 200px;
}
// JavaScript
window.layoutWorklet.addModule('my-layout.js');

Идеи

Promise

CSS Animation Worklet API

Демо parallax-animation

Web Animations

// JavaScript
const element = document.querySelector('.my-element');
element.animate([
	{'--some-color': 'red',  'opacity': 0 },
	{'--some-color': 'blue', 'opacity': 1 },
], {
	direction: 'alternate',
	duration: 5000,
	iterations: Infinity,
});

Polyfill

Web Animations
// worklet.js
registerAnimator(
	'scroll-position-animator',
	class {
		constructor(options) {
			this.options = options;
		}

		animate(currentTime, effect) {
			effect.localTime = currentTime;
		}
	});
// app.js

const frames = [
	new KeyframeEffect(
		document.querySelector('.scroll-to-top'),
		[
			{ 'transform': 'translateY(100px)', 'opacity': '0' },
			{ 'transform': 'translateY(0)', 'opacity': '1' }
		],
		{ duration: 10, iterations: 1, fill: 'both' }
	)
];
// app.js

const timeline = new ScrollTimeline({
	timeRange: 10,
	scrollSource: document.querySelector('.page-wrapper'),
	orientation: 'vertical',
	startScrollOffset: '200px',
	endScrollOffset: '300px',
});
// app.js

CSS.animationWorklet.addModule('worklet.js').then(() => {
	const animation = new WorkletAnimation(
		'scroll-position-animator',
		frames,
		timeline
	);
	animation.play();
});

Полифил anim-worklet

Демо scroll-animation

Где использовать?

Strength

Font Metrics API

Метрики шрифтов
const element = document.querySelector('.my-element');
	document.measureElement(element);
interface FontMetrics {
	double width;
	sequence<double> advances;
	double boundingBoxLeft;
	double boundingBoxRight;
	double height;
	double emHeightAscent;
	double emHeightDescent;
	double boundingBoxAscent;
	double boundingBoxDescent;
	double fontBoundingBoxAscent;
	double fontBoundingBoxDescent;
	Baseline dominantBaseline;
	sequence<Baseline> baselines;
	sequence<Font> fonts;
};
Happy

Почему Houdini хорош?

Недостатки Houdini

https://ishoudinireadyyet.com

Is Houdini ready yet‽

DasSurma

GoogleChromeLabs

houdini.glitch.me

css-houdini.rocks

Демо от Виталия Боброва

Материалы по теме

Статьи

  1. Interactive Introduction to CSS Houdini, Sam Richard
  2. Houdini: Maybe The Most Exciting Development In CSS You’ve Never Heard Of, Philip Walton
  3. What Houdini Means for Animating Transforms, Ana Tudor
  4. The CSS Paint API, Ruth John

Видео

  1. Philip Walton | Polyfills & Houdini | Browser API Special — CSS Day 2017
  2. Serg Hospodarets: CSS Houdini - from CSS variables to JavaScript and back — Frontend United 2017
  3. Patrick Kettner — Creating magic with Houdini — OdessaJS 2018

Спасибо за внимание!

@dark_mefody
n.a.dubko@gmail.com

QR code — ссылка на презентацию
Parser DOM / CSSOM Cascade Layout Paint Composite