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

Mercari Engineering Blog

メルカリのエンジニアブログです。技術情報を日々発信していきます。

未来のCSSを先取るHoudiniとは?それは魔法である!

CSS

f:id:t32k:20161130124906j:plain

こんにちわ、メルカリアッテでFront-end Developerをしている@t32kです。

メルカリではセミナー参加補助制度があり、それを利用して海外カンファレンスに参加してきました。今回は11/30 ~ 12/01、オーストラリア・メルボルンで開催されたCSS/JSConf Australia 2016に行ってきたので、そのレポートを書きたいと思います。

f:id:t32k:20161130154633j:plain

今回はその中でも、CSSConfでのBarak Chamo氏が講演した内容が非常に興味深かったので紹介します。

Hey presto, CSS!

今日はお話する内容は魔法についてです。どのように自分が定義したCSSをブラウザ上で利用可能にするのかという魔法です。

f:id:t32k:20161214215947p:plain

その前に、魔法ではないこれまでのCSSについて振り返ってみましょう。日本には『珍道具』と呼ばれるものがあります。とても素晴らしい技術ですが、ちょっとオーバーエンジニアリングに見えますね。これがどのように私たちのWeb技術に関連するのか見てみましょう。

border-radiusの例

数年前まで、角丸はWebデベロッパーにとっては夢のような技術でした。Web2.0に代表されるようなデザインでは必要でしたし、Appleも好んでこのような表現を用いていました。ほんとうに至るところで使用されていました。

しかし、問題はborder-radiusのブラウザ対応状況でした。皆が皆、最新のMacBookで最新のGoogle Chromeを使って閲覧すれば、それはとても素晴らしいWebサイトに見えるでしょう。しかし、もしそのサイトをIE6で閲覧するとどうなるでしょう?ホラーです。あなたのサイトは崩れて、まともに動きません。まさに大事故です。

f:id:t32k:20161214215929p:plain

border-radiusに対応していないブラウザのために、角丸部分を画像にすることでborder-radiusを再現することができます。メンドクサイですよね。個人的に、これはオーバーエンジニアリングに感じます。

CSS Standards

なぜこのようなことをしなければいけないのか?答えはCSSの標準化にあります。新しいCSSの機能はどうやって、私たちが使えるようになるのでしょう。

  • 機能の提案
  • 仕様の作成
  • ブラウザの実装
  • 対応ブラウザの普及
  • 使用可能!!

まずは機能の提案からですね。border-radiusは素晴らしいアイデアだと思います。私たちにとって必要なものです。次に詳細なAPIをどうするか仕様を作成します。Firefox、Chrome、Safariなどのブラウザベンダーがそれを実装します。そして、その機能を実装をしたブラウザが普及し始めて、ようやく私たちが使えるCSSとなるのです。

問題は、ブラウザの実装から普及までに大きな隔たりがあることです。ここで、新しいCSSはかならずしも普及することはなく、廃止になるかもしれません。また、仕様の確定、普及までに数年かかるかもしれません。Flexboxがまさにそうです。border-radiusは『あればいい』程度ですが、Flexboxのようなレイアウトに関わるものは必須条件です。CSS Regionsの対応状況は、依然としてよくありませんが、CSS Regionsを使用したサイトを古いブラウザで開けば、まさに大事故です。すべてが壊れてしまいます。

そこで私たちは仕様の提案段階で回避策を用意します。先述のとおり、border-radiusのような回避策は多くの手間や画像を用いることになり、オーバーエンジニアリングです。また、Flexboxの回避策にはJavaScriptを多用しなければならず、パフォーマンスを低下させてしまいます。

You Can Make CSS!

ポリフィルも回避策も必要とせず、自分で新しい機能を実装できたらいいと思いませんか?今回、紹介するHoudiniと呼ばれるタスクフォースが策定するAPIがそれを可能にします。Houdiniを理解する前に、まずCSSがどのように動作しているのか、理解する必要があります。

(Wikipediaによると、ハリー・フーディーニという奇術師がいたそうです。「現在でもアメリカで最も有名な奇術師」と呼ばれるほど認知度は高く、奇術師の代名詞となっていることから、魔法のような技術として、このような名前になったのでしょう。)

  1. Styles
    1. CSS OM
    2. Cascade
  2. Layout
  3. Paint
  4. Compositon

4つの段階があり、まずスタイルシートはDOMのようなCSSオブジェクトモデルに変換されます。それはプロパティ、クラス、エレメントといった情報を持っていて、カスケードされます。レイアウト段階では、ページ上のインライン・ブロック要素すべて、最終的な構造に配置されます。ペイント段階では、ブラウザに表示するためのビジュアルがレンダリングされます。コンポジット段階ではレイヤーがどのように配置するか決めます。

そうゆうわけで、HoudiniのAPIがどのように、これらのフェーズにアクセスできるのか、いくつか紹介したい思います。

CSS Parser API

まず、最初に紹介するのはCSS Parser APIです。

// スタイルの文字列を解析する
const color = cssParse.rule("color: orange");
console.log(color.styleMap.get("color").value); // "orange"

これはCSSの文字列を解析することで、CSSのルールをプロパティと値に分割して利用できるようになります。

// スタイルシートの解析
fetch("style.css")
    .then(response => CSS.parseStylesheet(response.body))
    .then(styles => console.log(styles))

同様に、外部スタイルシートの解析もJavaScriptで出来ます。

これには非常に興奮しています。なぜならこのAPIの目的は単にCSSを解析するのではなく、それを利用することで自分が考えた本物のCSSを追加することができるからです。

CSSの拡張

/* カスタム関数 */
--awesome(...)

/* カスタム@ルール */
@awesome('asd')

/* CSSライク属性 */
<img sizes="asdas asdaad" />

カスタム関数も追加できますし、メディアクエリーのようなカスタム@ルールを追加できます。また、img要素に新しい属性sizesを追加し、CSSのように複数の属性値を指定できたりします。これらは技術的な話ですが、みなさんは一体どのようなCSSを追加したいでしょうか?

/* クロマキー関数? */
--chroma-key(green) // OMG!

/* GIFフレーム選択? */
--gif-frame(10)

/* カスタムフィルタ? */
--insta-wow(hipsterWow)

/* .headerクラスからの継承 */
@inherit(.header)

/* カラースキームの生成 */
@palette {
    --base-color: orange;
    --harmony: complementary;
}

例として、私が考えるイケてるCSSを紹介しましょう。クロマキー関数があったらどうでしょう?緑色の背景の画像に対して、この関数を適用することで緑色の部分を切り抜くことができます。これはレンダリングエンジンに直接アクセスできるからですね。ですから、SassやLessなどのJavaScriptポリフィルも必要としません。他にもGIFのフレームを指定できたり、インスタグラムのようなフィルタ関数もあると楽しいかもしれません。

またCSSの言語自体拡張するのはどうでしょう?CSSで継承したり、カラースキームを生成したり、エキサイティングなことばかりです。

(CSS Parser APIはHoudiniタスクフォースから離れ、WICG:Web Incubator Community Groupのほうで策定が進むそうです)

CSS Properties and Values API

カスタムプロパティやCSS変数を聞いたことある人はいますでしょうか?少ないですね。

:root {
    --palette-root: orange
}

こういったものです。--palette-rootに対してorangeという文字列をあてています。ここで言いたいのは、あくまでもorangeは文字列にすぎないということです。

// カスタムプロパティの登録
CSS.registerProperty({
    name: '--palette-root',
    syntax: '<color>',
    inherits: true,
    initialValue: 'hsl(174, 90%, 66%)'
})

// JSからカスタムプロパティを設定
document.style.setProperty('--palette-root', 'lightblue')

HoudiniのAPIでは、より多くのインターフェースを備えており堅牢になっています。上記のコードでは、CSSエンジンに対して、このプロパティはcolorの型であることを伝えていたり、継承はするのか、しないのか、初期値なども設定できます。

body {
  --primary-theme-color: tomato;
  transition: --primary-theme-color 1s ease-in-out;
}
body.night-theme {
  --primary-theme-color: darkred;
}

これによるメリットは、colorの型を持つことで、CSSエンジンに対して正確な情報を伝えることが出来ます。上記のようなケースの場合、動的に--primary-theme-colorの値を変えても、期待した動作をするのです。

CSS Typed OM

CSSオブジェクトモデルは既存のCSSオブジェクトモデルを拡張することが出来ます。

// widthの取得
var width = Number(getComputedStyle($0).width.split('px'),[0])

// widthの設定
$0.style.setProperty('width','calc(50% + ' + width + ')')

皆さんはこのようなコードを見慣れているかもしれませんね。widthを取得するのに、数値の部分と単位の部分を分割しています。また、設定する場合は、calc関数の文字列として設定し、CSSエンジンが評価するという流れになります。これはとても非効率的です。もしブラウザの幅が変わったりすれば、都度このような無駄な処理が繰り返されるのです。

// 新しいスタイルマップ
$0.styleMap

// スタイルの値を取得
$0.styleMap.get('width')

// Widthの値を設定
let w = new CSSLengthValue(200, 'px')
$0.styleMap.set('width', w)

新しいAPIでは、値は型を持っていますので、それには意味があります。例えば、Widthの設定ですが、CSSLengthValueに値と単位を渡して初期化します。

// 型の混合
let w = new CSSLengthValue({
    percent: 50,
    px: -45
})
>> cale(50% - 45px)

もっと複雑なこともできますよ。パーセントとピクセルの2つの異なる単位を計算することもできます。なぜなら、CSSエンジンは型の情報を知っているからです。

// Lengthの計算
w = w.add(new CSSLengthValue(50, 'px'))
w = w.divide(2)

// Transformの操作
t = new CSSTransformValue()
t.add(
    new CSSRotation(
        new CSSAngleValue(angle, 'deg')
    )
)

Transformだって操作できますよ。より厳格なインターフェースが実装されることで堅牢かつ使い勝手がよくなっています。もう正規表現を使ってコードをいじくり回す必要はないのです。

CSS Layout API

Layout APIを使うことで、カスタムレイアウトを作ることができます。もしかしたら、みなさんはあまり興奮しないかもしれませんね。なぜなら既にJavaScriptを使うことで、そのようなレイアウトは現在でも作成可能ですから。しかし、このAPIを使用することでメインスレッドで実行されるのではなく、Workletsという別スレッドで動かすことで、よりハイパフォーマンスにカスタムレイアウトを実現することができます。

Layout APIのコードを見る前に、レイアウトがどのように定義されるのか、ボックスとフラグメントについて考えてみましょう。ボックスは文字通りの意味ですが、フラグメントというのがあります。例えば、一行の文章があったとします。これは一つのdivで構成されていますが、その中にも、また別のspanのようなインライン要素があります。このようなインライン要素は単語の途中で改行されることで2つの要素に別れることもあるのでフラグメントと呼称しています。

// 新しいレイアウトクラスを定義
class MyLayout {...}

// レイアウトクラスを登録
registerLayout('awesome', MyLayout)

Layout APIではカスタムレイアウトはJavaScriptのClassで定義します。コンテナの制限を決め、その中の要素であるフラグメントのレイアウトを計算します。そして登録します。

/* カスタムレイアウトの適用 */
.awesome-container {
 display: layout(awesome);
 display: layout(myFlexbox);
 display: layout(masonry);
}

layout関数を使うことで、カスタムレイアウトを適用することができます。これで独自のFlexboxを定義したり、Masonryスタイルのレイアウトも定義することが可能です。

CSS Paint API

// Paintクラスの定義
class Circle {...}

// Painterの登録
registerPaint('circle', Circle)

最後にPaint APIです。最初にPainterクラスを定義します。それはCanvas APIのような記述の仕方で記述することができます。

/* Painterの適用 */
.circle {
    --circle-color: blue;
    background-image: paint('circle');
}

背景画像に対して、Painterを適用します。


CSS Houdini - Paint API - Chat Bubble

Chromeチームが作ったデモです。このような感じで、すべてCSSプロパティで制御可能です。


CSS Houdini - Paint API - QR Code

QRコードの例も素晴らしいです。これは完全にCSSで出来ています。QRのURLを変更することで即座に反映されます。


Houdini’s Paint Worklet to paint a ripple

マテリアルデザインで有名なリップルエフェクトもHoudiniで定義することが出来ます。

What's the Point?

私が伝えたかったことのひとつは、クロスブラウザの互換性です。text-shadowやbox-shadowいろいろなCSSプロパティがブラウザ毎の進捗具合で実装されています。そのために私たちは個別に対応しなければなりませんが、もしHoudiniのAPIさえサポートされていれば、自分でCSSを実装することで、すべてのブラウザに対応することできます。

次に、ハイパフォーマンスなスタイルだと言うことです。今回紹介したAPIはメインスレッドとは別にWorkletというスレッドで実行するためハイパフォーマンスです。これまでのようなjQueryで実装しているレイアウトポリフィルでは、ブラウザのリフレッシュ、リサイズ毎、スクロール毎、マウスの操作ごとにブラウザに負荷をかけていることでしょう。新しいAPIではより早い段階でレイアウトを決定したり、より低レベルなAPIでアクセスすることで効率的になっています。最後に、クリエィティブな自分独自のCSSの機能を追加することができます。

ただ残念なことに、ほとんどのブラウザでHoudiniのAPIは現在、使えません。一部のAPIはChrome Canaryで"Experimental Web Platform features"を有効にすると使用可能になります。

またサンプルコードも公開されています。

まだまだ始まったばかりのプロジェクトですが、このようなAPIを率先して使うことは非常に重要です。なぜなら開発者がなにを欲しているのか、なにが嫌いなのか、ほんとうに重要なものをなにか、標準化チームに伝えることができるからです。

これらの仕様はすべて、GitHub上にあがっていますので、IssueをあげることやPull Requestを投げることもできます。これらの仕様策定には数年かかることかもしれませんが、一緒に歩んでいきましょう。ありがとうございました。

所感

CSS Houdiniという言葉自体は以前から知ってはいましたが、具体的にどういうものかは理解してはいなかったので、実際のデモやコード(とはいってもまだ仕様は不安定ですが)を見せてもらうことで、その可能性に気付かされました。このような流れは昨今のThe Extensible Webに沿うものであるので、Chrome Canaryで試すことができるAPIに関しては積極的に勉強してみたいですね。また、カンファレンス全体はアクセシビリティなどの基本的ところから、Houdiniなどのような最新技術を扱ったセッションもあり、とても良い構成のカンファレンスでした。

その他のすべてのセッションの動画は公式サイトでアップロードされているので参照してみてください。

f:id:t32k:20161202115513j:plain

あとメルボルン良い街でした(´ω`)