もっと細かい値を基準にしたVertical Rhythm
本文のline-height
を基数としたVertical Rhythmは、値が大きくなりすぎるのでどうしても細かい調整がしたくなってうまくいかない。
それに悩んでいて@terkelに聞いてみたら、「本文のline-height
より細かい数字を基準にしてる、4px
とか。Material Designも多分そういうルールに則ってる」という話を聞いた。なるほど良さそうと思ったものの、細かい数字になってくると計算がめんどくさそう。
Rhythmic Sizingを利用できれば、それはかなり簡単に実現できる。要素のサイズやline-height
の値を、必ず指定した数値の倍数になるようにできるというやつだ。以下はline-height
を4px
の倍数にする例。
html { line-height-step: 4px; } .text { font-size: 1rem; // 16px line-height: 1.7; // 27.2px => 28px } .heading { font-size: 3rem; // 48px line-height: 1.3; // 62.4px => 64px }
これが動くようになれば嬉しいんだけど、まだ実装されるかもわからないのでSassでline-height-step
もどきを作ってみる。
$line-height: 1.7; $line-height-small: 1.3; $font-size: 1rem; $font-size-large: 3rem; $rhythm-text-line: ($font-size * $line-height); $rhythm-unit: ($rhythm-text-line / 4); // step unit like // Reference: https://www.w3.org/TR/css-rhythm-1/#step-unit @function rhythm-step($length) { @if unit($length) != unit($rhythm-unit) { @error "The unit of $length should be #{unit($rhythm-unit)}"; } $steps: ceil($length / $rhythm-unit); @return ($rhythm-unit * $steps); } // unit less `line-height` // Reference: https://twitter.com/terkel/status/826600067930873857 @function rhythm-line-height($font-size, $line-height) { @if unit($font-size) != unit($rhythm-unit) { @error "The unit of $font-size should be #{unit($rhythm-unit)}"; } @if not unitless($line-height) { @error "$line-height should not have a unit"; } $rhythmical-length: rhythm-step($font-size * $line-height); @return ($rhythmical-length / $font-size); } .text { margin: 0 0 $rhythm-text-line; font-size: $font-size; line-height: rhythm-line-height($font-size, $line-height); } .heading { margin: 0 0 $rhythm-text-line; font-size: $font-size-large; line-height: rhythm-line-height($font-size-large, $line-height-small); }
rhythm-step
に与えた値は$rhythm-unit
の倍数になる。$rhythm-unit
の値は、px
にしたときに整数になるようにした方が安心できるんじゃないかとか、本文のfont-size
とline-height
の最大公約数になってた方が都合がいいんじゃないかとか考えると4px
にするのがちょうどいい気もする。けど、小さすぎる値になるとパターンとして認識されない気がしたので、少し大きめの本文のline-height
を4で割った値にしてる。
rhythm-line-height
は、line-height
から単位を取り除いた値に変換する。というのも、line-height
に絶対的な値を指定していると、フォントサイズが想定より大きい値になったときに行間が詰まりすぎてしまうからだ。具体的には、Chromeの最小フォントサイズ設定はデフォルトでは10pxになっているが、ユーザーがそれを16pxに設定しているとする。その場合、次のようになっていると問題が起こる。
.text { font-size: 10px; // => 16px line-height: 15px; }
単位無しの値を設定しておくと、リズムは崩れるものの、最低限の可読性を確保することはできる。JavaScriptで無理やり解決することはできそうだけど、そこまでのリスクを負ってやるべきことではなさそう。
ユーザーは常にスクロールしながらコンテンツを閲覧するので、視点を一定のリズムで制御できる利点は失われるという意見もあるので、どの程度のコストをかけて実装していくべきなのかは考えたい。
エモさ on 地味な土台 in ウェブサイト
地味な土台の部分をきちんと作った上に、いわゆるエモさみたいなものが積み上げられた形のウェブサイトを作りたいなというのをずっと思ってる。
地味な土台というのは、アクセシビリティやパフォーマンス、サイト全体を通してのスタイルの一貫性など、ウェブサイトを作るために普通に意識すべきことのことを指している。
というのも、最近のウェブサイトを作るときの意識って、気持ちいいアニメーションがあって、インタラクティブに動いて、見た人が魅了されるみたいなエモさを追求することに向かい過ぎてる。その結果、当たり前にやるべきはずのことが蔑ろにされている。しかし、エモさはあくまで付加価値であって、それ自体を最優先で考えるのはウェブサイトの目的を見失っていることに他ならないと感じる。
なんでその目に見えない地味な部分を見た目にわかりやすいエモい部分より優先しないといけないのかというと、それが他のメディアにはないウェブの強みを活かすために必要な要素だからだ。ウェブは普遍的であることで発展してきた。誰もがどこからもアクセスできて、いつでもちゃんと使える。これはごく当たり前のことに聞こえるけど、多くはそれを意識できてないか無視してる。
反して、ウェブの利点を犠牲にしてまでウェブサイトのエモさを追求したところで他のメディアには敵わない。縦横比が固定された動画や、加えてプラットフォームが限定されたゲームなどと違って、多様なユーザーエージェントに対応するためには点で見たときの見た目や体験だけをあてにできない。だから、発想の柔軟性は制限されているし、コストも掛かり過ぎる。
ユーザーの多様性を認めることがウェブの良さを最も素直に発揮できる方法であって、全てのウェブサイトはその前提の下で設計されるべきだ。
とは言いつつも、僕はウェブサイトにおけるエモさを否定はしてないし、それを取り入れること自体は良いことだと思ってる。けど、そのために別の環境やユーザーからはアクセスできない、使えないようになってしまうなら採用すべきでない。
ウェブサイトを制作するために必要なことをピラミッドで表すと、地味な土台の上にエモさは位置している。下の層から順に達成できている状態で、その先でエモさを作り出すことに取り組めれば最も理想的。そんな優先順位でウェブ制作に取り組むことが当たり前になれば、みんなにとって魅力的なウェブにしていけると僕は信じてます。
CSSにおける和欧混植のベストプラクティス
欧文はウェブフォント、和文はユーザーエージェントのデフォルトにするというのがベスト。
html { font-family: Lato, sans-serif; }
というのも、ローカルの欧文フォントを指定するためにはプラットフォームごとにインストールされているフォントを把握しておく必要があるからだ。現状のシェアを占めているものだけに絞ればまだしも、クライアント環境が多様化し続けている昨今ではそのアプローチは不十分に思える。
ちなみに、主要なプラットフォームにインストールされているフォントの一覧は以下。
ウェブフォントを利用した指定にしておけば、プラットフォームに関わらず和欧混植にできる。読み込みに失敗するというケースも考えられるが、その場合の見栄えは妥協できるだろう。
和文の指定に関しては、まずウェブフォントはコストが高いので避けたい。加えて、前述したような理由でフォントファミリー名を指定するのも微妙。そこでsans-serif
などのジェネリックな指定にしておけば、ユーザーエージェントが最適なフォントを選択してくれる可能性が高い。
とはいえ、タイポグラフィとしては和文フォントと欧文フォントの組み合わせがまずいものにならないようにはしたい。そういう場合は、それぞれのフォントが利用できる場合のみ特定の処理を行うというアプローチも取れる。
参考:
Sassのmixinでユニークなキーフレーム名を宣言する
スプライトアニメーションをコンテンツの背景にフィットさせる - ライデンの新人ブログ
こういうやつを複数回やりたかったのでmixinにした。普通にやるとキーフレーム名がかぶるので、mixinを呼び出すたびにIDをインクリメントさせるようにしたらいけた。
$_sprite-id: -1; @mixin sprite($frame-count, $seconds-per-frame) { $_sprite-id: $_sprite-id + 1 !global; background-size: 100% ($frame-count * 100%); animation: sprite-#{$_sprite-id} #{$frame-count * $seconds-per-frame}s steps($frame-count) infinite; @keyframes sprite-#{$_sprite-id} { from { background-position: left top; } to { background-position: left (100% * $frame-count / ($frame-count - 1)); } } } .sprite1 { @include sprite(3, .1); background-image: url("sprite1.png"); } .sprite2 { @include sprite(12, .2); background-image: url("sprite2.png"); }
keyframesの内容が同じでも複数回宣言するので雑だと思うけど、ちゃんとやるのめんどくさそうなのでやめた。Mapで$frame-count
をキーにしてIDを保存しとくとかすればいけそう。
2017年に向けてやってきたこと
去年の今頃ってなにしてたんだっけって思ったら、倒れて手術してたんだった。これを機に、人はいつでも死ぬのでなんかやるなら早めが良さそうという気持ちになったんだった。
この頃の僕には、自分のフロントエンドの専門性を高めたいという目標があった。とはいえ、専門学校に入ってからウェブサイトのコーディングとかするようになって2年目という程度の経験しかなくて、それでも実際にプロとしてやってる人たちに絶対に負けたくない気持ちがあった。そして、負けないためには短期間で効果的に成長する必要があった。そのために僕は、とにかく考える物事の範囲を狭めて、獲得した成長が必ず専門性を深める結果になるように意識した。短期的にはその試みは成功したと思ってる。
そうこうやってるうちに少し遅れて就職した。そこで感じたのは理想と現実の剥離だった。
僕が専門性を高めることを意識してきた理由は、自分が担当する役割の中においてベストな結果を実現させるためだ。けれど現実には、自分の担当範囲だと思っていた部分の外に見過ごせないだけのたくさんの問題があった。そんな状況のもとでは、仮に自分の手だけを正しく動かしていても、最終的な成果物を正解に近づけられない。現状を変えるためにもっといろんなことを考えないと良いものが作れないという意識に変わってきた。
入社してから今日までは、それらをいかにして改善していくかを考えてきた毎日だった。
僕がGitHubに公開してるReal World Website Boilerplateはそのひとつだ。他の人が作った開発環境が作業しづらくてかなりストレスを感じていたので、こうやれば快適に開発できるという枠組みを考えて提案するために公開している。「ぼくがかんがえたさいきょうのgulpfile.js
」みたいなのは本質的でない部分に労力を割いているようで馬鹿にされがちだけど、その結果どうすれば開発しやすい形になるか考えることが軽視されて、関わりたくないつらいプロジェクトになっていく。あんまり開発環境にこだわりがない人に向けて「これ使ってたらおおよそ間違いないです」と薦めたい。
最近は会社で場を設けてもらって、ウェブはこうやって作っていくべきみたいな話をできるだけ具体的にしてる。それを叩き台にして全員で議論したり、加えて、これまでのやり方を改善して形にするための試験的なプロジェクトもやってみてる。
改善の手がかりが掴めたような気がして、変化の兆候が見え始めた気がしてる。
tween.jsでキャンセルできる直列なアニメーションを実装する
const TWEEN = require('tween.js') const [tween] = [...document.querySelectorAll('.animate')] .map(el => { const state = {x: 0} return new TWEEN.Tween(state) .to({x: 300}) .onUpdate(() => { el.style.transform = `translate(${state.x}px, 0)` }) }) .map((currentTween, i, tweens) => { const nextTween = tweens[i + 1] if (nextTween) currentTween.chain(nextTween) return currentTween }) tween.start() document.querySelector('.stop').onclick = () => tween.stop() requestAnimationFrame(function animate(time) { requestAnimationFrame(animate) TWEEN.update(time) })
気の利かないライブラリだとsindresorhus/p-cancelableとかでcancelableなPromisesにする感じなのかなと思うけど、tween.jsは丁度いいAPIなので楽にできた。
Browserifyでbundleしたファイルをconcatする
Browserifyでrequire
じゃなくて単にconcatしたいときがある。UMD対応してないライブラリを読み込むときとか、必ず最初に実行したい処理(ポリフィルとか)がある場合だ。
Webpackだと、以下のようにentry
に配列を指定すればconcatできる。
module.exports = { entry: [ 'picturefill', 'src/js/index.js', ], output: { filename: 'bundle.js', path: './dist', }, }
とはいえBrowserifyを使いたい。以下のようにしたら、Gulpでストリームとしていい感じに捌けた。
const gulp = require('gulp') const gutil = require('gulp-util') const sourcemaps = require('gulp-sourcemaps') const concat = require('gulp-concat') const uglify = require('gulp-uglify') const mergeStream = require('merge-stream') const browserify = require('browserify') const source = require('vinyl-source-stream') const buffer = require('vinyl-buffer') export const js = () => { const ENTRY_FILES = [ 'node_modules/picturefill/dist/picturefill.js', ] const bundler = browserify('src/js/index.js', { debug: true, }) const bundle = () => mergeStream( gulp.src(ENTRY_FILES), bundler .bundle() .on('error', err => gutil.log('Browserify Error', err)) .pipe(source('index.js')) .pipe(buffer()), ) .pipe(sourcemaps.init({loadMaps: true})) .pipe(concat('app.js')) .pipe(uglify({preserveComments: 'license'})) .pipe(sourcemaps.write('.')) .pipe(gulp.dest('dist/js')) return bundle() }
追記:上記のやり方だとStreamの解決順にconcatされてしまうので絶対に安全では無い。Browserifyでbundleするよりもgulp.src
に時間がかかることはまずないけど、不安になるコードだった。gulp-headerとかでやったら良さそう。
const fs = require('fs') const gulp = require('gulp') const gutil = require('gulp-util') const sourcemaps = require('gulp-sourcemaps') const header = require('gulp-header') const uglify = require('gulp-uglify') const browserify = require('browserify') const source = require('vinyl-source-stream') const buffer = require('vinyl-buffer') export const js = () => { const ENTRY_FILES = [ 'node_modules/picturefill/dist/picturefill.js', ] const concatedScripts = ENTRY_FILES.map(file => fs.readFileSync(file, 'utf8')) .concat('') .join('\n') const bundler = browserify('src/js/index.js', { debug: true, }) const bundle = () => bundler .bundle() .on('error', err => gutil.log('Browserify Error', err)) .pipe(source('app.js')) .pipe(buffer()) .pipe(sourcemaps.init({loadMaps: true})) .pipe(header(concatedScripts)) .pipe(uglify({preserveComments: 'license'})) .pipe(sourcemaps.write('.')) .pipe(gulp.dest('dist/js')) return bundle() }
これだと、ENTRY_FILES
のログがソースマップでうまいことできないけど、たぶんこっちのほうがよさげ。