CSS APIを操作する (CSS Houdini)

ここ最近のChromeのアップデートでHoudiniプロジェクトで進められている機能が試すことが可能となったということでざっと眺めてみた。

HoudiniではブラウザのCSS APIを開発者に解放することを目的としている。
最終的にはW3Cでの標準化を目指しているプジジェクト。
APIを使うことで開発者はブラウザベンダーと同じく、自由にブラウザのCSSレンダリングの低レベル部分を操作できるようになり、自由な表現が可能となる。

Houdiniの仕様

ドラフトによると以下のAPIで構成されている。

  • Box Tree API 1
  • CSS Layout API 1
  • CSS Painting API 1
  • CSS Parser API 1
  • CSS Properties and Values API 1
  • CSS Typed OM 1
  • Font Metrics API 1
  • Worklets 1

API経由で開発者の書いたコードがブラウザのCSSエンジンのフックされ、カスタムCSSを作成し実行することができる。
カスタムCSS機能はWorkletと呼ばれ、JavaScriptで定義されている。
Workletはブラウザ実行時にJavaScriptとしてブラウザにロードされる。
ユーザーはあたかもブラウザ組み込まれたスタイルのようにスタムCSSを利用できる。

WorkletはWebworkerに似ているが、次の部分が違う。

  • メインスレッドの依存しないため特定のスレッドで実行するというようなことはできない
  • 並列化のためにグローバルに複数の重複するインスタンスを生成できる
  • イベントAPIではなく、UserAgentに呼び出される

ブラウザのサポート状況 (2018年3月現在)

各ブラザベンダーの実装状況はIs Houdini ready yet‽で知ることができる。

Worklet

Houdiniに関連するWorkletは次のものがある。

name content
PaintWorklet CSSのカスタムプロパティのレンダリングを定義
AnimationWorklet カスタムのアニメーション、スクロールの定義
LayoutWorklet カスタムのレイアウトの定義

指定されたURIにあるworkletを取得ためのaddModuleメソッドが提供されている。

1
2
3
4
5
<credentials> =  'omit' | 'same-origin' | 'include'
<option> = 'credentials'

const option {credentials: 'omit'}
Promise = Worklet.addModule(workletURI, option);

それぞれに関してかいつまんで見る。
機能は chrome:// flagsExperimental Web Platformの機能を有効にすることが必要になる。

PaintWorklet

任意のboxに対して描画を行うAPI。CanvasのレンダリングAPIのサブセットを使い描画行う。

AnimationWorklet

以前はCompositorWorkerとして提案されていたもので、
worklet内でカスタムのアニメーションの実行を行う。
こちらはアニメーションのサブクラスを使いアニメーションを行い、複数のタイムラインを持つことができる。

LayoutWorklet

レイアウトやボックスモデルのサイズ、positionなどを開発者で計算ができるようになる。

CSS Typed OM

workletではないが、JavaScriptからCSS単位付きの値(px, %など)演算を行う際に
オーバーヘッドが発生するため、CSSの値を型付きでAPIとして公開し開発者に提供するもの。

例えばこのようなな感じで扱っていた値とユニットを

1
2
3
4
const elem = docuemnt.queryselector('#element')
let w = elem.style.width.slice(0,-2)
w++
elem.style.width = `${w}px`;

CSSStyleValueをもちいることで以下のように扱うことができる。

1
2
3
4
const elem = docuemnt.queryselector('#element')
let w = element.attributeStyleMap.get('width');
w.value++;
element.attributeStyleMap.set('width', w);

またユニットの違う場合の計算においても、cssのcalc関数のように取得できる。

1
2
// like calc(1em + 5px)
const width = new CSSMathSum(CSS.em(1), CSS.px(5));

Paint APIを使ったデモ

ボーダースタイルの動的変更

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Houdini</title>
</head>
<body>
<style>
.paint-button {
display: flex;
margin: 2rem auto;
font-size: 1.5rem;
padding: .8em;
border-radius: .25rem;
border-style: solid;
border-width: 3px;
border-image-slice: 10;
/* paint api */
border-image-source: paint(paint-button, orange, 10);
}
</style>
<div id="paint-button">
<button class="paint-button">button</button>
</div>
<script>
if (
'paintWorklet' in CSS && typeof CSS.paintWorklet.addModule === 'function'
) {
CSS.paintWorklet.addModule('paint-button.js');
} else {
console.log('Please enabled CSS property');
}
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// paint-button.js
registerPaint('paint-button', class {
// css paint apiに渡されるプロパティ
// CSS Properties の syntax strings の type
static get inputArguments() {
return [
'<color>',
'<number>'
]
}

paint (context, geometory, properties, args) {
const color = args[0].value;
const lineWidth = args[1].value
context.lineWidth = lineWidth;
context.strokeStyle = color;
context.strokeRect(0, 0, geometory.width, geometory.height);
}
});

paint(paint-button, orange, 10); でモジュール名と引数を渡す。
workletのcallbackはJavaScriptのclassとなり、引数はinputArgumentsで列挙される。
ここではタイプを指定して取得できる。指定できるタイプはcss-properties-valueのsupported-syntax-stringsにあるものが指定可能。
pain()メソッドでカスタムの描画を行うことができる。


Ripple Button デモ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Houdini</title>
</head>
<body>
<style>
.ripple-button {
--circle-radius: 0;
--circle-color: #fff;
--bgcolor: #6eb9f7;
background: var(--bgcolor);
margin: 2rem auto;
display: flex;
color: #1b2e3d;
cursor: pointer;
border: 1px solid #61a5dd;
font-size: 1.5rem;
padding: .8em;
border-radius: .25rem;
background-image: paint(circle-ripple);
outline: none;
}
</style>
<div id="ripple">
<button class="ripple-button">button</button>
</div>
<script>
if (
('paintWorklet' in CSS && typeof CSS.paintWorklet.addModule === 'function') &&
('registerProperty' in CSS && typeof CSS.registerProperty === 'function')
) {
CSS.paintWorklet.addModule('ripple.js');
// {
// name: 変数名,
// syntax: 型,
// inherits: 継承,
// initialValue: 初期値
// }
const customProps = [
{name: '--ripple-x', syntax: '<number>', initialValue: false, initialValue: '0'},
{name: '--ripple-y', syntax: '<number>', initialValue: false, initialValue: '0'},
{name: '--ripple-radius', syntax: '<number>', initialValue: false, initialValue: '0'},
{name: '--ripple-color', syntax: '<color>', initialValue: false, initialValue: '#6eb9f7'}
];

// カスタムプロパティを適用
customProps.forEach(prop => {
CSS.registerProperty(prop)
});

const button = ripple.querySelector('.ripple-button');

button.addEventListener('click', event => {
button.classList.add('animating');
button.style.setProperty('--ripple-x', event.offsetX);
button.style.setProperty('--ripple-y', event.offsetY);
});

// transitoon終了後
button.addEventListener('transitionend', () => {
button.classList.remove('animating');
});
} else {
console.log('Please enabled CSS property');
}
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ripple.js
registerPaint('circle-ripple', class {
// アクセスできるプロパティの列挙
static get inputProperties() {
return [
'--ripple-x',
'--ripple-y',
'--ripple-radius',
'--ripple-color'
];
}

constructor() {
this.prop = {
name: 'circle-riple'
}
}

paint(context, geometory, properties, args) {
const x = properties.get('--ripple-x').toString();
const y = properties.get('--ripple-y').toString();
const radius = properties.get('--ripple-radius').toString();
const color = properties.get('--ripple-color').toString();
context.fillStyle = color;
context.arc(
x,
y,
radius,
0,
2 * Math.PI
);
context.fill();
}
});

イベントのcallbackと組み合わせることで、tansition効果をつけたエフェクトも可能。
cssのカスタム変数へのアクセスはinputPropertiesで列挙できる。

まとめ

  • ブラウザベンダーが提供してないビジュアルやエフェクトを独自で実装可能となる
  • 複雑なDOMやCSSプロパティを使用することなく高度なビジュアルやエフェクトを実装可能
  • ブラウザベンダーより先に実装するを実現することができる
  • ビジュアルを抽象化してモジュールとして提供することができる

ということで、これまでのCSSでのリッチUIの提供方法に変化がある未来がすぐそこまで来ている。
Webcomponentsと同じようにビジュアルのモジュール化できるので、これまでのCSS開発手法が変わってきそう。

参考

css-properties-values-api-1
ss-houdini-drafts
Houdini – CSS の秘密を解き明かすもの

Comments