Re: なぜピクセルパーフェクトは筋が悪いのか

昨日書いた記事はあまりに雑だった。それに対するつぶやきを見て、思考が整理されたので改めて書く。

ピクセルパーフェクトが正しくないのは、カンプの時点ではまだデザインは未完成であるからだ。つまりは、未完成なデザインをそのまま形にすることを強制していることになる。

GUIのデザインツールとCSSは、それぞれ別のアプローチでデザインを表現できる手段だと言える。

GUIは自由に発想を展開して試行錯誤するためのプレイグラウンドだ。あえて制約を緩めることによって、さまざまな形を実験する効率を高めている。反して、実際の媒体の制約と照らし合わせると現実性の無いものもできる。制約は意識しにくいし、ルールで縛りたい場合には向いてない。そのための仕組みが弱いからだ。

CSSはルールを適応するためのツールだ。指定した要素に対して一律同じスタイルを当てることができる。デザインを構成するルールは論理的な単位で分解して再利用できる。反面、デザインが理路整然としていないとCSSは難しくなる。痛みに最も敏感になれる場所だとも言える。例外に弱い。

良いデザインを作るためには、たくさんの試行錯誤と一貫したルールが必要だ。それを実現するためには、互いを組み合わせてデザインするのが最も強い。

GUIツールを使って作られたカンプを基にCSSを書くことも立派なデザインの過程である。カンプを作ることとはまた別の視点での。それら一通りを同じ意志を持った人が行うことで、初めてひとつのデザインが完成する。

分業は大きな壁だ。いわばデザインの途中段階で、その完成を他人に委ねてしまうことになる。うまく進めるには可能な限り思考を同期させるしかない。

けど、正しい答えにたどり着くための、文脈を理解する手がかりや、そこで見つけた間違いをフィードバックするための仕組みは全く未熟なままだ。ウェブにおいて差し迫った問題として活発に議論されているが、それでもまだまだ重要性が認識されていないことがほとんどだ。

問題を解決するための答えはそれぞれの場所に必ずある。互いのメンタルモデルを深く理解することが、壁を取り払って本質的な問題に目を向けるための第一歩になる。

なぜピクセルパーフェクトは筋が悪いのか

  • カンプは実装するために作る中間成果物
  • 実装時にはスタイルにブレがあると都合が悪い。デザインとしても破綻している
  • 余白やフォントサイズのルールなど、デザインツールでは一律再現するのが難しい
  • スタイルシートという概念を持ってすることで、デザインツールでは不可能な一貫性を表現できる
  • あくまでカンプはデザインの印象を伝えるだけのもの。細かいルールのブレはCSSを書くときに正規化すべき

後日続きを書いた

シンプルなレスポンシブデザインを実現するためのメディアクエリの考え方

スタイルはシンプルなものから宣言していく方が無駄がない。

レスポンシブデザインにおいては、小さい画面のほうがより簡素な画面構成であることが多い。そのため、小さい画面から順に大きい画面まで設計したほうが理に適っている。例えば以下のようなコードになる。

$breakpoint-small: 480px;
$breakpoint-medium: 960px;

.component {
  margin: 1rem;
  padding: 1rem;
  border: 1px solid lightgray;

  @media (min-width: $breakpoint-small) {
    margin-bottom: 3rem;
  }

  @media (min-width: $breakpoint-medium) {
    font-size: larger;
  }
}

このような実装にすることで、小さい画面のスタイルは引き継ぎつつ、上書きする必要があるプロパティだけ宣言すればよくなる。複数のクエリを組み合わせる((min-width: 576px) and (max-width: 767px)など)よりも、適用されるスタイルが把握しやすくて簡単になる。

f:id:yuheiy:20170605185018p:plain

この場合、小さい画面のほうがスタイルが複雑な要素(ハンバーガーメニューなど)があっても、メディアクエリの書き方は一貫させたほうが良い。むしろあまりにそういったケースが多いのであれば、デザインが間違っていることを疑うべきだ。

カンプを作るときや、コーダーとのやりとりをするときにも、この考え方を知っている前提だとうまくいく。幅480px未満ではこれ、幅480px以上で幅960px未満のときはこうなる、という風にできると円滑になる。

縦書きでレスポンシブなブログを作った

趣味で会社の人のサイトを作った。縦書きでレスポンシブなブログ。prismic.ioNext.jsで作った。

ウェブデザインに縦書きを活かすことは難しい。部分的に取り入れることはできても、縦書きの文章を主要な要素として扱うのはかなり難がある。というのも、ウェブサイトは縦にスクロールするのが当たり前だけど、普通に縦書きで実装すると横スクロールになるからだ。

横書きでは文章は上から下に流れ、ページは縦スクロールになる。対して縦書きの場合、文章は右から左に流れるため、横スクロールになる。スクロール操作が不自然だと目に見えてユーザービリティが低下するので、どうしても当たり前のスクロールができるようにしたい。

幸い、縦書きにしながら縦スクロールにする方法はひとつある。新聞や雑誌のように段組にすることだ。

それはcolumnsを利用すれば、一見簡単にできそうな感じはする。けど、僕が望む仕様を実現しようとすると案外難航した。僕の今回の実装は次のような仕様になっている。

  • 縦書き
  • 段が最初の右端から左端、あふれたら次の段からという風に順番に埋められる
  • 文章の量には依存しない
  • レスポンシブ

columnsは、全ての段の高さを揃えようとする。そのため、最初の段から順番にブロックの中を埋めていくことはできない。ハック的に解決するしかなかった。

.ArticleBody {
  width: 100%;
  max-width: 100%;
  height: 14rem;
  writing-mode: vertical-rl;
  columns: 14rem;
}

高さを固定することで、最初の段からはみ出そうになるまで別の段にテキストが流れることはなくなる。はみ出した分は次の段へ移り、おおよそ望む形になる。

ただそうすると、要素自体の高さは一段分にしかならない。これでは次に続く要素がこの要素と重なってしまう。JavaScriptで要素の高さ分のpaddingを設定することで解決した。

const adjustSize = el => {
  const fullHeight = el.scrollHeight
  const defaultHeight = Number(getComputedStyle(el).height.replace('px', ''))
  el.style.paddingBottom = `${fullHeight - defaultHeight}px`
}

scrollHeightは要素のスクロール分も含めた高さを返す。正直これまでscrollHeightの存在を知らなかったんだけど、コンソールに勘で$0.scro...とか打ってたら候補で出てきたのでびっくりした。

この段組の仕組み自体はIE11を含むクロスブラウザで動作する(このサイトでは対応してない)。ただ、Firefoxの挙動が壊れていて、デベロッパーツールでスタイルを付け外しすると直るって感じだった。今回はsetTimeoutで擬似的に再現した。

ついでに言うと、Firefoxは縦書き周りのバグがかなり厳しくて、今回作ったサイトではFirefoxだけ別のデザインにした部分がある。あと、CSS VariablesとかLogical Propertiesを使いたかったので、IEとEdgeは無視した。

この段組の実装は結構珍しいアイデアな気がしてて、さっき縦書きアワードのやつ一通りみたけどやってる人がいなかったので世界初の可能性がある。なんか金とか欲しい。

Firefoxの実装が直ったら仕事でも使えなくは無いかな、という感じだと思う。ちょっと怖いけど。

ちなみにcolumn-fill: autoではやりたいようにならなかった。


裏側の仕組みもちょっと遊んでみてる。

このブログの記事は僕が書いていくという訳ではない。なのでGitにマークダウンのファイル作るとかじゃなくて、CMSで管理したいということだった。

まともにWordpressとかでCMS環境を作るのはつらいしやりたくない。データベースとか勝手に管理してくれて、REST APIだけ提供してくれるようなのないかなーと思ってたら、案外いろいろあるらしい。なんとなくprismic.ioというやつを選んだ。

使い方は、カスタムフィールドみたいなのをJSONで定義して(GUIもある)、後はそれに合わせたフォームを提供してくれるのでデータを追加していくだけ。基本的にはいい感じなんだけど、フォームにIME周りのバグが結構あって、使えても社内ツールくらいかなと思った。

他にもTumblrとかGoogle Spread Sheetで管理するのも試したけど、これの優位点は自由にフォーマットを決められて画像の管理もしてくれるところだった。

CMSの更新時にはWebhookを飛ばしてくれるので、そのときにCIでビルドしてデプロイしたら静的サイトとして作れる。けど、ビルド環境作るのがだるくてさぼった。代わりにNext.jsを使った。

Next.jsはReactでIsomorphicなウェブアプリを作るためのフレームワーク。ビルド環境とかルーティングの仕組みとかが組み込まれてて、驚異的に楽。ただ、CSS in JSが前提なので普通のCSSを書く環境作るのがめんどくさかったのは良くなかった。CSS in JSの類のものがCSSの根本的な難しさを解決するとは思わない。SPAの人たちがみんなそこに向かってるのはどうなんだろう……。


ということで結構楽しかったです。ソースとかここです。

ウェブサイトにおけるタイポグラフィのパターンの設計

ウェブサイトのタイポグラフィのルールは一貫性を欠いてしまいがちだ。

単純にページやコンポーネント単位でデザインしていると、その周辺の要素だけで判断することになるので、全体として見ると一貫性を失ってしまう。

僕が考える解決策は、コンポーネントからは独立したタイポグラフィだけを定義するルールセットを作ること。そして全てのコンポーネントは必ずいずれかのルールセットに従うようにする。

@mixin typography-heading {
  font-size: 2rem;
  line-height: 1.3;
}

.article-heading {
  @include typography-heading;
}

ルールセットは必ずセマンティックな(意味のある)命名をする。見た目に依存しない命名だと、デザイン変更のたびに大幅にコードを変更しなくて済むことが多い。そして、情報として同じ意味を持つ要素は、タイポグラフィも一貫させるという意図を強調させるためだ。

これに加えて、反復の原則をより意識すると、ルールセット内で利用する値はデザイン全体で共通のものにしたい。これをコードで表現すると、あらかじめ利用する可能性のある値は全て定義しておいた上で、ルールセットはその値を参照するだけになる。

$breakpoint-small: 480px;

$font-size-root: 16px; // browser default

$font-size: 1rem;
$font-size-x-small: (12px / $font-size-root * 1rem);
$font-size-small: (14px / $font-size-root * 1rem);
$font-size-large: (24px / $font-size-root * 1rem);
$font-size-x-large: (32px / $font-size-root * 1rem);
$font-size-xx-large: (48px / $font-size-root * 1rem);
$font-size-xxx-large: (64px / $font-size-root * 1rem);
$font-size-xxxx-large: (96px / $font-size-root * 1rem);
$font-size-xxxxx-large: (128px / $font-size-root * 1rem);

$line-height: 1.7;
$line-height-small: 1.3;

この段階で定義する値は、意味を持たない単なるサイズでしかないため、基準になる値からの相対的な命名にする。これらの値は自身がどこから参照されるかには興味がない。

line-heightのバリエーションはこれくらいシンプルでも問題ないと思っている。Vertical Rhythmを意識するといろいろあるけど、設計の単純化のためには考えない方がいい。

@mixin typography-heading {
  font-size: $font-size-x-large;
  line-height: $line-height-small;
}

@mixin typography-sub-heading {
  font-size: $font-size-large;
  line-height: $line-height-small;
}

@mixin typography-lead {
  @media (min-width: $breakpoint-small) {
    font-size: $font-size-large;
  }
}

@mixin typography-caption {
  font-size: $font-size-small;
}

@mixin typography-button {
  font-size: $font-size-small;
  line-height: $line-height-small;
}

@mixin typography-display {
  font-size: $font-size-x-large;
  line-height: $line-height-small;

  @media (min-width: $breakpoint-small) {
    font-size: $font-size-xx-large;
  }
}

先述したように、これらはセマンティックな命名をした上で、単に先ほど定義した値を参照するだけ。メディアクエリによるスタイルの変更は、できればやらない方が設計としては単純になる。が、これはさすがに避けられないのでこの層に書く。

html {
  line-height: $line-height;
}

.article-heading {
  @include typography-heading;
}

.article-body {
  // inherit from root
}

.article-footer {
  @include typography-caption;
}

.article-more-link {
  @include typography-button;
}

.page-heading {
  @include typography-display;
}

.page-summary {
  @include typography-lead;
}

実際のコンポーネントからは、先ほど定義したmixinincludeするだけになる。ルートからfont-sizeline-heightをそのまま継承して使えるものには何もしない。


当然これで答えだと思ってないので、みなさんのいろいろな意見を聞かせてください。

参考
hail2u/hail2u.net
twbs/bootstrap
Shopify/polaris
Material Design

font-familyに指定するフォントスタックの一般的なパターン

font-familyのベストな書き方20XX年みたいな記事が毎年出るけど、実際のところケースバイケースで、唯一のベストな解があるというわけでない。なので個人的な理解に基づいたパターンの分類をメモとして書いておく。特に目新しいことはない。

日本語環境で、ウェブフォントは考慮しないという前提。

和文フォントのみ

html {
  font-family: "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
}

たぶん最も無難。

游ゴシックはいろいろあって不安なので、特に寿命が長いウェブサイトでは採用したくない。次のバージョンのOSでは状況が変わったりしそうだから。

メイリオWindows環境で利用するため。MS Pゴシックは基本的には絶対悪で、アンチエイリアスがかからず可読性が悪いので避けたい。すると和文フォントはメイリオ択一になる。Windows環境のブラウザでは、sans-serifのデフォルトがメイリオになりつつあるんだけど、まだまちまちなので明示的に指定してあげないといけない。

ヒラギノMaciOSのデフォルトだけど、メイリオより高い優先順位にするために指定する。

メイリオWindowsのデフォルトになってくれれば、これはsans-serifと指定するだけで事足りるようになる。

和欧混植

html {
  font-family: "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
}

上記のスタックに欧文フォントを加えただけ。

従属欧文はあまりよく思われないことが多いので、欧文フォントを和文フォントより優先して指定する。選択肢はいろいろあるけど、この辺がよく使われる。

システムフォント

html {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

デバイスのシステムフォントとして採用されているフォントをウェブサイトにも利用する例。

近年のCSSフレームワークなどではよく採用されていて、上記の記事にあるMediumの他に、GitHubとかQiitaとかでもこれ。

結果的にこれも和欧混植になることが多いけど、分類のために便宜上区別してる。


フォントスタックを選択するときの考え方は、たぶんこういう分類にするのが適切だと思う。

わざと引っかかりのあるアニメーションを実装する

一般的にはアニメーションは高パフォーマンスであることが良しとされる。が、あえてFPSを下げたアニメーションにしたいことがあった。レトロゲームのような世界観を表現したいというやつだった。

tween.jsは、ユーザーがアニメーションの更新タイミングを制御できるAPIになっている。こういう感じ。

var coords = { x: 0, y: 0 };
var tween = new TWEEN.Tween(coords)
    .to({ x: 100, y: 100 }, 1000)
    .onUpdate(function() {
        console.log(this.x, this.y);
    })
    .start();

requestAnimationFrame(animate);

function animate(time) {
    requestAnimationFrame(animate);
    TWEEN.update(time);
}

つまりTWEEN.updateを呼ぶ頻度を制御できればいい。それを踏まえてこういう風になる。

const createFpsControlledTicker = (handleTick, fps = 60) => {
  let startTime = null
  let lastFrame = -1
  const secondsPerFrame = 1000 / fps
  let requestId = null
  let isRunning = false

  const tick = () => {
    requestId = requestAnimationFrame(tick)

    if (startTime == null) startTime = performance.now()

    const currentTime = performance.now()
    const elapsedTime = currentTime - startTime
    const currentFrame = Math.floor(elapsedTime / secondsPerFrame)

    if (lastFrame !== currentFrame) {
      handleTick(currentFrame)
      lastFrame = currentFrame
    }
  }

  const start = () => {
    if (isRunning) return
    requestId = requestAnimationFrame(tick)
    isRunning = true
  }

  const stop = () => {
    if (!isRunning) return
    cancelAnimationFrame(requestId)
    isRunning = false
  }

  return {
    start,
    stop,
  }
}

createFpsControlledTicker(() => {
  TWEEN.update()
}, 15).start()

以上のようなコードで15fpsのアニメーションを実装できた。