読者です 読者をやめる 読者になる 読者になる

The future starts today

Webとか英語とか育児とかに関する雑記

Houdini、それはCSSの進化を促すプロジェクト

JavaScript CSS

この記事は CSS Advent Calendar 2016 の5日目の記事です。
W3C Houdini Task Forceで進められている「Houdini」と呼ばれるプロジェクトの話をします。

FlexBoxの例

f:id:shibe97:20161202210321p:plain

突然ですが、FlexBoxの話をします。 モジュールの横並びには重宝しますよね。 今年になってだいぶ利用が進んだ印象がありますが、随分と前からFlexBoxの仕様は存在していました。

一番最初の草案に遡ってみると、なんと2009年。
7年前です。

2013年くらいからFlexBox良いぞという記事はちらほら出始め、おそらく皆認識はしていました。
が、ブラウザの対応状況などを考慮し実装できずにいたと思います。 仕様定義の議論から実際にここまで普及するまで7年間もかかっているわけです。

流れを整理してみると、以下のようになります。

f:id:shibe97:20161202210428p:plain

提案、議論、仕様書作成あたりに時間がかかるのは仕方がないと思いますが、ブラウザベンダーによる実装やユーザーの利用状況に大きく影響を受けるのはあまりよろしくありません。

何が問題かというと、「後方互換性がない」ことだと思います。

一方、JavaScriptは…

TC39という団体により、ステージング形式が採用されています。
各仕様の策定具合は4つのstageで表されており、毎年の策定時点でStage4に到達していたものが、その次のバージョンのECMAScriptとして入ってきます。

  • Stage1: proposal
  • Stage2: draft
  • Stage3: candidate
  • Stage4: finished

JavaScriptJavaScript自身でPolyfillを作ることができます。 また、Babelを用いて、未対応のブラウザでも新機能が使えるように変換することができます。

つまり、新仕様が実装されてもPolyfillをJS自身で書けるので「後方互換性」があります。

CSSでは同じことができないの?

CSSの場合は多くの処理がブラウザ本体に依存しているため、Polyfillを作ろうとしても作れません。

そこで出てきたのが今回紹介するHoudiniというプロジェクトです。

Houdiniとは

簡単に言うと、JavaScriptからブラウザ本体をいじれるようにしよう!というプロジェクトです。 ブラウザの低レベルAPIを提供し、JavaScriptからブラウザの描画アクションにフックさせ、CSSからアクセス可能にします。

ブラウザのレンダリングの仕組みは以下のようになっています。

f:id:shibe97:20161202210445p:plain

このうち、基本的にはDOMにしかJavaScriptからアクセスすることができません。(一部CSSOMも可能)

そこで、DOM以外の部分もJavaScriptからコントロール可能にし、CSSの制御に干渉できるようにします。

f:id:shibe97:20161202210457p:plain

※これから紹介するものは草案段階のため、今後大いに変更される可能性があります
※また、Editor's draftを元に解説している部分が多いため、参考程度だと思ってください

一つずつ見ていきましょう。

CSS Properties and Values API(2016/12/3時点)

Editor’s draft : https://drafts.css-houdini.org/css-properties-values-api/

  • 仕様あり、Chrome/Firefoxにて開発中
  • CSSカスタムプロパティに型や初期値などを設定できる
CSS.registerProperty({
  name: "--my-color",
  syntax: "<color>",
  initialValue: "black"
});

もし--my-colorというカスタムプロパティを以下のように使おうとした場合、

.thing {
  --my-color: green;
  --my-color: url("not-a-color");
  color: var(--my-color);
}

2つめのurl(“not-a-color”)はinvalidとなるため、1つめのgreenが適用されます。

CSS Parsing API(2016/12/3時点)

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

  • 仕様はあるが、ブラウザ実装はまだない
  • CSS3の解析アルゴリズムを公開する
  • CSS Typed OMより簡単で緩やかに型付けされた表現で結果を返す

CSS Typed OM(2016/12/3時点)

Editor’s draft : https://drafts.css-houdini.org/css-typed-om/

  • 仕様あり、Chromeで一部実装あり、Firefoxで開発中
  • CSSの属性と対応する値型をkey-value形式のオブジェクトとして保持できる
  • 属性のバリデーションは実行時に使われる
  • px, em, rem, % などを相互に変換可能になる

Worklet

要素のLayout、Paint、Compositeを司る部分に関しては、Workletという仕組みが導入されます。
Web Workerに似ていて、レンダリングエンジンがWorkletを動かします。
(ただし、オーバーヘッドが大きいので利用は控えめにするべきです)

以下のようなregisterXXX()というAPIをコールし、グローバルスコープにClassとして処理を登録します。
(各UserAgent間でスコープを守るためにメソッドではなくClassを登録する形式にしています)

  • registerLayout
  • registerPaint
  • registerAnimator

グローバルスコープ、つまりwindowオブジェクトに各Workletは紐づいており、各Workletはimportというモジュール読み込みメソッドを持っています。

例:

window.paintWorklet.import('paintworklet.js');

このimportまわりではセキュリティ的な懸念があります。

CSS Layout API(2016/12/3時点)

Editor’s draft : https://drafts.css-houdini.org/css-layout-api/

  • 仕様はあるが、ブラウザ実装はまだない
  • ノードの子をノードのボックス内でどう配置するかを定義できる
  • cssのdisplayプロパティを自作できる
  • flexやtableのようなものを作れる
registerLayout('some-layout', class {
    *layout(space, children, styleMap, breakToken) {
        
        // ...

        return {
            blockSize: blockOffset,
            inlineSize: inlineSize,
            fragments: childFragments,
            // ...
        };
    }
});

regiterLayoutというAPIを用いてレイアウトを登録し、CSSから利用する。

CSS Painting API(2016/12/3時点)

Editor’s draft : https://drafts.css-houdini.org/css-paint-api/

上記のrippleのコード

registerPaint('ripple', class {
  static get inputProperties() { return ['background-color', '--ripple-color', '--animation-tick', '--ripple-x', '--ripple-y']; } 
  paint(ctx, geom, properties) { 
    const bgColor = properties.get('background-color').cssText; 
    const rippleColor = properties.get('--ripple-color').cssText; 
    const x = parseFloat(properties.get('--ripple-x').cssText); 
    const y = parseFloat(properties.get('--ripple-y').cssText); 
    let tick = parseFloat(properties.get('--animation-tick').cssText); 
    if(tick < 0) 
      tick = 0; 
    if(tick > 1000) 
      tick = 1000; 
    ctx.fillStyle = bgColor; 
    ctx.fillRect(0, 0, geom.width, geom.height); 
    ctx.fillRect(0, 0, geom.width, geom.height); 
    ctx.fillStyle = rippleColor; 
    ctx.globalAlpha = 1 - tick/1000; 
    ctx.arc( 
      x, y, // center
      geom.width * tick/1000, // radius
      0, // startAngle
      2 * Math.PI //endAngle
    ); 
    ctx.fill(); 
  } 
});
<button id="ripple"> 
  Click me! 
</button> 
<style>
  #ripple {
    width: 300px;
    height: 300px;
    border-radius: 150px;
    font-size: 5em;
    background-color: rgb(255,64,129);
    border: 0;
    box-shadow: 0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);
    color: white;
    --ripple-x: 0;
    --ripple-y: 0;
    --ripple-color: rgba(255,255,255,0.54);
    --animation-tick: 0;
  }
  #ripple:focus {
    outline: none;
  }
  #ripple.animating {
    background-image: paint(ripple);
  }
</style>
<script>
  window.paintWorklet.import('paintworklet.js');
  const button = document.querySelector('#ripple');
  let start = performance.now();
  let x, y;
  document.querySelector('#ripple').addEventListener('click', evt => {
    button.classList.add('animating');
    [x, y] = [evt.clientX, evt.clientY];
    start = performance.now();
    requestAnimationFrame(function raf(now) {
      const count = Math.floor(now - start);
      button.style.cssText = `--ripple-x: ${x}; --ripple-y: ${y}; --animation-tick: ${count};`;
      if(count > 1000) {
        button.classList.remove('animating');
        button.style.cssText = `--animation-tick: 0`;
        return;
      }
      requestAnimationFrame(raf);
    })
  })
</script>

registerPaint APIでpaint関数を定義し、cssのbackground-colorプロパティから呼び出すことができます。
registerPaint内ではCanvasライクなAPIを用いて描画処理を記述することが可能です。

CSS Compositor API(2016/09/26時点)

解説:https://dassur.ma/things/animworklet/

  • 仕様はあるが、ブラウザ実装はまだない
  • Compositor Worklet の仕様は WICG(Web Incubator Community Group) に移され、Animation Workletに置き換わった
  • レイヤーの重ね合わせのタイミングでスクロール位置やtransform、opacityなどにアクセス可能
  • パララックススクロールなどの実装が可能になる
  • Polyfillによるデモ(なので実際Animation Workletは使われていない):http://googlechrome.github.io/houdini-samples/animation-worklet/sync-scroller/

registerAnimator APIを用いて処理を定義します。

WICGによる解説: github.com

Font Metrics(2016/12/3時点)

Editor’s draft : https://drafts.css-houdini.org/font-metrics-api/

  • 仕様はあるが、ブラウザ実装はまだない
  • フォントのバウンディングボックスのサイズを取得できる

f:id:shibe97:20161202210543p:plain

各仕様のブラウザ実装状況

こちらにまとまっています。 https://ishoudinireadyyet.com/

2016/09/26時点では以下のようになっています。

f:id:shibe97:20161202210603p:plain

まとめ

Houdiniと聞くとWorkletの方に注目が集まってしまいますが、実際に仕様を見てみると、カスタムプロパティの登録であったり、型定義の部分が重要なアップデートだと感じました。

Houdiniによって様々なPolyfillの作成は可能になると思いますが、Workletの部分は実案件として使うにはパフォーマンス面などに影響がありそうです。
また、実装例を見ていても、CSS以上にJavaScriptの記述量が多く、なかなか骨が折れる印象です。
現段階で遊べそうなのはPainting APIくらいでしょうか。
Layout APIが個人的には気になりますが、まだ動かせる環境が無いのが残念です。

Houdiniをあくまで仕様の安定化を促進するためのものと考えれば、有意義なものであると思います。

まだまだ議論の途中段階だと思うので、今後もちょくちょく追っていきたいと思います。