CSSだけでviewportに常に対角線を引く

画面に対角線をどうしても引きたい時が人生に一度や二度は来ると思う。

任意の角度で斜め線を引くならlinear-gradient()<color-stop>を工夫したり、線だけの要素をtransform: rotate()するなど手法はいくつかある。しかしviewportの対角線においては閲覧環境によってその角度が異なるので、角度を動的に算出する必要がある。

一見望みがありそうなcalc()はどうだろうか。平方根や三角関数を直接求めることは2016年4月時点ではできないが、CSS Variablesを利用し、計算結果を保存して別のcalc()に渡していけばできるかもしれない。

長方形と対角線の長さと角度の関係

viewportを長方形と見ると、幅が100vwで高さが100vhとなる。これで斜辺Cの長さがわかれば、対角線の上側と下側の角度がラジアン単位で求められるので、linear-gradient()の第一引数やrotate()に指定すれば解決だ。

なのだが、前述した通り平方根を直接求めることはできない。 平方根を逐次計算で求めていく方法もあるようだが、100vwの二乗、100vhの二乗とはいったいいくつなのだろうか? Computed Valueでは実数になっているけど、それはComputed Valueでの話だし、CSS Varで--num: calc(100vw * 100vw);としても、乗算では引数は少なくとも1つが<number>でなければならないのでエラーになる。

ということで、どう頑張ろうともCSSだけで動的に角度を求めることはできない。

でもCSSでできるよ

できないのは「動的に角度を求めること」なので、違うアプローチで対角線を表現する。

具体的には、borderプロパティで三角形を作る方法を応用し、直角三角形を重ねて対角線を表現する。

CodePenにデモを作った。

Windowsユーザーの人はtoggle menuボタンを押すとスクロールバーが出てアレするが、それについては後述しているので一旦置いておいてほしい。

やってることを言葉で詳しく説明すると、

  • 100vw * 100vhの大きさの要素を作る
  • その要素は上下のborder-width50vhずつ、左右のborder-width50vwずつである
  • border-leftborder-bottomのみにborder-colorをつける(他はtransparent
  • 同じような要素をもう一つ重ねて、topleftから1pxずつずらす
  • 上に重なっている要素のborder-colorは、コンテンツエリアの背景色と同じにする

となる。右上から左下への対角線の場合はborder-colorで色を指定する箇所を変えればよい。

重なりのイメージ

内容物の幅と高さが共に0で、要素の大きさがborder-width分しかない場合、そのborder-width分の大きさの要素が作られ、隣接するborderの間には色の境界線ができる。そして対角に位置する方の「border同士の境界線」と直線で結ばれる。境界線の角度は要素の大きさで決まり、要素の大きさはborder-widthの合計値で100vw100vhになっているので、viewportに対角線が引かれているのと同義になる。 ちょっとわかりやすくなるかもしれないサンプルも作った。ウィンドウサイズをぐいんぐいんすれば対角線が維持される様子がわかると思う。

デモでは画面に対してposition: fixedで固定配置しているので、スタック文脈の解決のためにコンテンツエリアもposition: relativeする必要があるので注意。

ウィンドウサイズぐいんぐいんおじさんがデスクトップ版Safariを使っている場合、「ウィンドウサイズをぐいんぐいんすると対角線が追随しないぞ」というバグ報告を絶対にしてくる。なので強制再描画ハックもつけた。これでウィンドウサイズぐいんぐいんおじさんも満足するはずだ。FirefoxやChromeでは強制再描画しなくてもウィンドウサイズぐいんぐいんに対角線が追随するので偉い。

問題点

まずブラウザのCSS対応的にViewport Unitが必要というのが絶対条件になる。 border-widthには<percentage>型は許容されておらずborder-width: 50%のような指定は使えないのでViewport Unitに対応していないブラウザでは諦めざるを得ない。

また、border-colorが単色しか指定できないので、コンテンツの背景も単色でなければならない。背景画像があるとかグラデーションを入れているとかだと対応できない。

そして一番の問題点は「スクロールバーで隠れる」ことと「Mobile Safari 9のUIで隠れる」ことだ。

問題の前者は主にWindows環境下での話。ページにスクロールバーが現れていると「見た目の画面幅」と100vwの結果が異なるというやつ。Windowsで デモのtoggle moreのボタンを押せばコンテンツが増えてスクロールバーが出るので、どういうことかわかると思う。

Edgeでスクロールバーが表示されていない場合

Edgeでスクロールバーが表示されている場合

問題の後者はiOS環境下で、Mobile Safari 9のUI(戻るボタンとかインテントボタンとかのやつ)がviewportにかぶさって「見た目の画面高」と100vhの結果が異なるというやつ。ロケーションバーの方ではなく画面の下側に出る方のUIが問題になる。ページをスクロールするとそのUIは隠されて見た目と結果が揃うのでやっかい。Mobile Safari 9に依存した問題なのでモバイル版のGoogle Chromeで閲覧すればこの問題は起きないが、それでいいのかと手斧が飛んできたら防げずに死ぬ。

Mobile SafariでUIが表示されていない場合 Mobile SafariでUIが表示されている場合

スクロールバーにしろボタンエリアにしろ、ブラウザUIを除いた表示領域の大きさをCSSだけで動的に得るには限界がある。特に今回は対角線の描画にborderを使っているので、%指定ができない点が痛い。

「CSSでできる」と言いながらも、できているのはOS Xでだけである。それ以上でも以下でもなく、他のことを一切解決できないまま結局死ぬ。


CSSお絵描きの未来

Pure CSSでアートワーク的なことはこれまで様々なものが紹介されてきた。しかし本気でやろうとしたらJavaScriptの補助は必要になってくる。今回のように閲覧環境やコンテンツの状況によって変わる値を必要とする場合はなおさらだ。

CSSの表現の幅の拡張で、その界隈ではCSS Houdiniに一瞬注目が集まったような気がする。でも結局中ではJavaScriptで実装を書くんだから、JSからは逃れられないのは変わらない。カスタムファンクションとしてCSSだけで動いているように見えるだけだ。

CSSお絵描きに未来はあるか。あるとすれば高等数学をサポートしたcalc()が登場するか、ブラウザUIを考慮した表示領域を扱える単位かプロパティーが登場する時だろう。しかしCSS Houdiniが策定されていくとなれば、そちらでやれということにしかならなそうな気もする。未来なさそう。